fix: reject unavailable self-operated collection commands before initialization

This commit is contained in:
2026-10-12 20:45:31 +08:00
parent 3b137f8946
commit d782b2cd24
4 changed files with 139 additions and 11 deletions
@@ -26,7 +26,7 @@ class SelfopCommandServiceTest {
configure();
var original = original("synthetic-account", "WH-B");
when(tasks.selectOne(any())).thenAnswer(call -> { com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<SelfopSyncTask> q=call.getArgument(0); q.getSqlSegment(); return q.getParamNameValuePairs().containsValue(original.getSourceAccount())?original:null; });
assertThatThrownBy(() -> service.request(retry(), "42")).hasMessage("SELFOP_TASK_SCOPE_MISMATCH");
assertThatThrownBy(() -> service.request(retry(), "42")).hasMessage("任务不存在或不属于当前账号和仓库");
verify(taskService, never()).retry(anyLong(), anyString(), anyString(), anyString());
verify(taskService, never()).create(any(SelfopSyncTask.class));
assertThat(original.getStatus()).isEqualTo("DEAD");
@@ -36,7 +36,7 @@ class SelfopCommandServiceTest {
configure();
var original = original("different-source-account", "WH-A");
when(tasks.selectOne(any())).thenAnswer(call -> { com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<SelfopSyncTask> q=call.getArgument(0); q.getSqlSegment(); return q.getParamNameValuePairs().containsValue(original.getSourceAccount())?original:null; });
assertThatThrownBy(() -> service.request(retry(), "42")).hasMessage("SELFOP_TASK_SCOPE_MISMATCH");
assertThatThrownBy(() -> service.request(retry(), "42")).hasMessage("任务不存在或不属于当前账号和仓库");
verify(taskService, never()).retry(anyLong(), anyString(), anyString(), anyString());
verify(taskService, never()).create(any(SelfopSyncTask.class));
assertThat(original.getStatus()).isEqualTo("DEAD");
@@ -77,16 +77,17 @@ class SelfopCommandServiceTest {
configure();config.setCollectionMode("OFF");
assertThat(config.isExecutionEnabled()).isFalse();
var c=new SelfopCommandService.Command("disabled","WH-A","INVENTORY",null,null,null,null,null,"offline test");
assertThatThrownBy(()->service.request(c,"42")).hasMessage("SELFOP_PURPOSE_INVALID");
assertThatThrownBy(()->service.request(c,"42")).hasMessage("采集功能未启用,当前环境不允许执行采集或重试");
assertThatThrownBy(()->service.request(retry(),"42")).hasMessage("采集功能未启用,当前环境不允许执行采集或重试");
config.setCollectionMode("BUSINESS");config.setEnvironment("test");
assertThat(config.isExecutionEnabled()).isFalse();
assertThatThrownBy(()->service.request(c,"42")).hasMessage("SELFOP_BUSINESS_ENVIRONMENT_REJECTED");
verify(taskService,never()).create(any(SelfopSyncTask.class));
assertThatThrownBy(()->service.request(c,"42")).hasMessage("采集功能未启用,当前环境不允许执行采集或重试");
verifyNoInteractions(taskService,scopes,accounts,tasks);
}
@Test void validationModeRejectsFormalInventoryEntry() {
configure();config.setCollectionMode("VALIDATION");
var c=new SelfopCommandService.Command("wrong-purpose","WH-A","INVENTORY",null,null,null,null,null,"offline test");
assertThatThrownBy(()->service.request(c,"42")).hasMessage("SELFOP_VALIDATION_ENTRY_REQUIRED");
assertThatThrownBy(()->service.request(c,"42")).hasMessage("当前采集模式与所选操作不一致,请使用对应入口");
verify(taskService,never()).create(any(SelfopSyncTask.class));
}
private SelfopCommandService.Command retry() {
@@ -13,6 +13,17 @@ import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.server.ResponseStatusException;
import java.util.Set;
import com.baiyue.common.web.GlobalExceptionHandler;
import com.baiyue.lastleg.selfoperated.analytics.model.SelfopSupport;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.core.MethodParameter;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.bind.support.WebDataBinderFactory;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
@@ -88,6 +99,79 @@ class SyncControllerTest {
assertThat(method.getAnnotation(PreAuthorize.class).value()).isEqualTo("hasAuthority('" + SYNC + "')");
}
@Test void offAndRetryAreChineseBusinessRejectionsBeforeAnyInitialization() throws Exception {
var config=new SelfopAnalyticsProperties();
var tasks=mock(SelfopTaskService.class);var scopes=mock(SelfopSyncScopeMapper.class);
var accounts=mock(SelfopOutboundDiscoveryScopeMapper.class);var rows=mock(SelfopSyncTaskMapper.class);
var mvc=mvc(new SelfopCommandService(tasks,scopes,accounts,config,rows));
rejected(mvc,command,"采集功能未启用,当前环境不允许执行采集或重试");
rejected(mvc,new SelfopCommandService.Command("retry","WH-A","RETRY","LXCOL-1",null,null,null,null,"原因"),
"采集功能未启用,当前环境不允许执行采集或重试");
verifyNoInteractions(tasks,scopes,accounts,rows);
}
@Test void unconfiguredWarehouseAndInvalidPublicInputsRejectBeforeInitialization() throws Exception {
var config=new SelfopAnalyticsProperties();config.setCollectionMode("BUSINESS");config.setEnvironment("production");
var tasks=mock(SelfopTaskService.class);var scopes=mock(SelfopSyncScopeMapper.class);
var accounts=mock(SelfopOutboundDiscoveryScopeMapper.class);var rows=mock(SelfopSyncTaskMapper.class);
var mvc=mvc(new SelfopCommandService(tasks,scopes,accounts,config,rows));
rejected(mvc,command,"该仓库尚未配置采集,暂不能提交任务");
config.getWarehouses().put("WH-A",new SelfopAnalyticsProperties.Warehouse());
rejected(mvc,new SelfopCommandService.Command("invalid","WH-A",null,null,null,null,null,null,null),"不支持此采集操作");
rejected(mvc,new SelfopCommandService.Command("invalid","WH-A","BACKFILL",null,null,null,null,null,null),
"采集时间范围不能为空,结束时间必须晚于开始时间");
rejected(mvc,new SelfopCommandService.Command("invalid","WH-A","RETRY","LXCOL-1",null,null,null,null," "),
"重试原因不能为空且不能超过500个字符");
verifyNoInteractions(tasks,scopes,accounts,rows);
}
@Test void controlledTaskConflictIsBusinessRejectionButUnknownFailureRemainsSystemError() throws Exception {
var config=new SelfopAnalyticsProperties();config.setCollectionMode("BUSINESS");config.setEnvironment("production");
config.setSourceAccount("synthetic-account");config.getWarehouses().put("WH-A",new SelfopAnalyticsProperties.Warehouse());
var tasks=mock(SelfopTaskService.class);var scopes=mock(SelfopSyncScopeMapper.class);
var accounts=mock(SelfopOutboundDiscoveryScopeMapper.class);var rows=mock(SelfopSyncTaskMapper.class);
var scope=new SelfopSyncScope();scope.setId(10L);scope.setScopeHash("hash");when(scopes.selectOne(any())).thenReturn(scope);
var mvc=mvc(new SelfopCommandService(tasks,scopes,accounts,config,rows));
doThrow(new IllegalStateException("SELFOP_IDEMPOTENCY_CONFLICT")).when(tasks).create(any());
rejected(mvc,command,"请求标识已用于其他内容,请刷新后重新提交");
doThrow(new IllegalStateException("unknown runtime failure")).when(tasks).create(any());
mvc.perform(post("/api/platform/v1/last-leg/self-operated-analytics/sync-tasks")
.contentType("application/json").content(SelfopSupport.json(command)))
// The platform handler transports all result codes in HTTP 200; unknown failures retain S0500.
.andExpect(status().isOk()).andExpect(jsonPath("$.code").value("S0500"));
}
@Test void runningRetryIsControlledAndDoesNotInitializeScopes() throws Exception {
var config=new SelfopAnalyticsProperties();config.setCollectionMode("BUSINESS");config.setEnvironment("production");
config.setSourceAccount("synthetic-account");config.getWarehouses().put("WH-A",new SelfopAnalyticsProperties.Warehouse());
var tasks=mock(SelfopTaskService.class);var scopes=mock(SelfopSyncScopeMapper.class);
var accounts=mock(SelfopOutboundDiscoveryScopeMapper.class);var rows=mock(SelfopSyncTaskMapper.class);
var original=new SelfopSyncTask();original.setId(1L);original.setTaskRole("WAREHOUSE_SYNC");
original.setWarehouseCode("WH-A");original.setExecutionPurpose("BUSINESS");original.setStatus("FETCHING");
when(rows.selectOne(any())).thenReturn(original);
doThrow(new IllegalStateException("SELFOP_RETRY_LEASE_BUSY")).when(tasks).retry(1L,"retry","42","原因");
rejected(mvc(new SelfopCommandService(tasks,scopes,accounts,config,rows)),
new SelfopCommandService.Command("retry","WH-A","RETRY","LXCOL-1",null,null,null,null,"原因"),
"任务正在执行,暂不能重试");
verify(tasks,never()).initialize();verify(tasks,never()).create(any());verifyNoInteractions(scopes,accounts);
}
private void rejected(MockMvc mvc,SelfopCommandService.Command request,String message) throws Exception {
mvc.perform(post("/api/platform/v1/last-leg/self-operated-analytics/sync-tasks")
.contentType("application/json").content(SelfopSupport.json(request)))
.andExpect(status().isOk()).andExpect(jsonPath("$.code").value("B0001"))
.andExpect(jsonPath("$.message").value(message));
}
private MockMvc mvc(SelfopCommandService commands){
return MockMvcBuilders.standaloneSetup(new SyncController(commands)).setControllerAdvice(new GlobalExceptionHandler())
.setCustomArgumentResolvers(new HandlerMethodArgumentResolver(){
public boolean supportsParameter(MethodParameter parameter){return parameter.getParameterType()==AuthenticatedUser.class;}
public Object resolveArgument(MethodParameter parameter,ModelAndViewContainer container,NativeWebRequest request,WebDataBinderFactory factory){
return user(0L,Set.of(SYNC));
}
}).build();
}
private AuthenticatedUser user(Long customerScope, Set<String> permissions) {
return new AuthenticatedUser(42L, customerScope, 0L, 1L, "synthetic-user", "PLATFORM", "WEB", permissions);
}