feat(selfop): add bounded inventory flow diagnostic through account egress

This commit is contained in:
2026-10-15 11:37:49 +08:00
parent e19d303a76
commit d81d9cbda4
16 changed files with 365 additions and 5 deletions
@@ -0,0 +1,22 @@
package com.baiyue.lastleg.selfoperated.analytics.controller;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.lastleg.selfoperated.analytics.sync.InventoryFlowProbeService;
import org.junit.jupiter.api.Test;
import java.util.Set;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.security.access.prepost.PreAuthorize;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class PlatformInventoryFlowProbeControllerTest {
@Test void probeRequiresRealPlatformSyncPermission() throws Exception {
var service=mock(InventoryFlowProbeService.class);var c=new PlatformInventoryFlowProbeController(service);
var reader=new AuthenticatedUser(1L,0L,0L,1L,"reader","PLATFORM","WEB",Set.of("platform:self-operated-analytics:list"));
var customer=new AuthenticatedUser(1L,1L,1L,1L,"customer","CLIENT","WEB",Set.of("platform:self-operated-analytics:sync"));
assertThrows(ResponseStatusException.class,()->c.probe(null,null));assertThrows(ResponseStatusException.class,()->c.probe(reader,null));
assertThrows(ResponseStatusException.class,()->c.probe(customer,null));verifyNoInteractions(service);
var writer=new AuthenticatedUser(1L,0L,0L,1L,"operator","PLATFORM","WEB",Set.of("platform:self-operated-analytics:sync"));
c.probe(writer,null);verify(service).probe(null,"operator");
assertEquals("hasAuthority('platform:self-operated-analytics:sync')",PlatformInventoryFlowProbeController.class
.getMethod("probe",AuthenticatedUser.class,InventoryFlowProbeService.Request.class).getAnnotation(PreAuthorize.class).value());
}
}
@@ -44,6 +44,21 @@ class SelfopEgressServiceTest {
success(c,Operation.CUSTOMERS);success(reserve(Operation.PRODUCTS,100,null,null),Operation.PRODUCTS);
assertThat(budget().confirmedHttp).isEqualTo(2);
}
@Test void flowProbeIsTestOnlyBoundToFrozenHashAndExactlyOneAttempt(){
testBusiness(5,300);task.setTaskType("INVENTORY_FLOW_PROBE");task.setRequestHash(INTERNAL);
assertThatThrownBy(()->reserve(Operation.OUTBOUND_QUERY,100,null,null)).hasMessage("EGRESS_FLOW_PROBE_INVALID");
assertThatThrownBy(()->reserve(Operation.INVENTORY_FLOW,1000,"OTHER",null)).hasMessage("EGRESS_FLOW_PROBE_INVALID");
assertThatThrownBy(()->service.reserve(context(),Operation.INVENTORY_FLOW,1000,"A",null,PROVIDER)).hasMessage("EGRESS_FLOW_PROBE_INVALID");
var c=reserve(Operation.INVENTORY_FLOW,1000,"A",null);success(c,Operation.INVENTORY_FLOW);
assertThatThrownBy(()->reserve(Operation.INVENTORY_FLOW,1000,"A",null)).hasMessage("EGRESS_FLOW_PROBE_INVALID");
assertThat(businessBudget().http).isEqualTo(1);assertThat(budget().confirmedHttp).isEqualTo(1);
}
@Test void flowPermissionFailureDoesNotPauseExistingSalesAccount(){
testBusiness(5,300);task.setTaskType("INVENTORY_FLOW_PROBE");task.setRequestHash(INTERNAL);
var c=reserve(Operation.INVENTORY_FLOW,1000,"A",null);service.authorize(new AuthorizationRequest(c,Operation.INVENTORY_FLOW,PROVIDER));
service.complete(new CompletionRequest(c,Operation.INVENTORY_FLOW,PROVIDER,1,"LINGXING_WMS_FLOW_PERMISSION_DENIED",200,11008,false,null));
assertThat(account.getApiPauseReason()).isNull();assertThat(budget().confirmedHttp).isEqualTo(1);
}
@Test void ordinaryTasksCannotUseMasterDataOperations(){
business();assertThatThrownBy(()->reserve(Operation.CUSTOMERS,10,null,null)).hasMessage("EGRESS_OPERATION_TASK_MISMATCH");
assertThatThrownBy(()->reserve(Operation.PRODUCTS,10,null,null)).hasMessage("EGRESS_OPERATION_TASK_MISMATCH");
@@ -0,0 +1,55 @@
package com.baiyue.lastleg.selfoperated.analytics.sync;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.LocalDateTime;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import static org.mockito.ArgumentMatchers.*;
import com.baiyue.lastleg.selfoperated.analytics.model.*;
import com.baiyue.lastleg.selfoperated.analytics.mapper.*;
import com.baiyue.lastleg.selfoperated.analytics.client.RestLingxingAnalyticsClient;
import org.springframework.transaction.support.TransactionTemplate;
class InventoryFlowProbeServiceTest {
@Test void successfulProbeIsAuditedReplayedAndNeverPublished() throws Exception {
var cfg=new SelfopAnalyticsProperties();cfg.setEnvironment("test");cfg.setCollectionMode("BUSINESS");cfg.setSourceAccount("demo");
cfg.getWarehouses().put("W",new SelfopAnalyticsProperties.Warehouse());
var tasks=mock(SelfopSyncTaskMapper.class);var runs=mock(SelfopSyncRunMapper.class);var accounts=mock(SelfopOutboundDiscoveryScopeMapper.class);
var leases=mock(SelfopLeaseService.class);var client=mock(RestLingxingAnalyticsClient.class);var tx=mock(TransactionTemplate.class);
doAnswer(i->{((java.util.function.Consumer<org.springframework.transaction.TransactionStatus>)i.getArgument(0)).accept(null);return null;}).when(tx).executeWithoutResult(any());
var a=new SelfopOutboundDiscoveryScope();a.setId(1L);when(accounts.selectOne(any())).thenReturn(a);
var lease=new SelfopLeaseService.Lease(1L,"probe",2L,true);when(leases.acquireDiscovery(eq(1L),anyString())).thenReturn(lease);
var saved=new java.util.concurrent.atomic.AtomicReference<SelfopSyncTask>();
when(tasks.selectOne(any())).thenAnswer(i->saved.get());when(tasks.insert(any(SelfopSyncTask.class))).thenAnswer(i->{saved.set(i.getArgument(0));return 1;});
var page=new ObjectMapper().readTree("{\"current\":1,\"size\":1000,\"pages\":1,\"total\":1,\"records\":[{\"id\":\"1\"}]}");
when(client.inventoryFlow(any(),anyString(),anyString(),anyString(),any(),any())).thenReturn(page);
var s=new InventoryFlowProbeService(cfg,tasks,runs,accounts,leases,client,tx);
var end=LocalDateTime.of(2026,1,1,0,0);var q=new InventoryFlowProbeService.Request("one","W","C","S",end.minusDays(30),end);
var first=s.probe(q,"operator");assertTrue(first.complete());assertEquals(first,s.probe(q,"operator"));
assertEquals("SAMPLE_DONE",saved.get().getStatus());assertNull(saved.get().getResultPublishedRunId());
var capture=org.mockito.ArgumentCaptor.forClass(SelfopSyncRun.class);verify(runs).insert(capture.capture());
assertEquals("SAMPLE_DONE",capture.getValue().getStatus());assertNull(capture.getValue().getPublishedAtUtc());assertNull(capture.getValue().getInventoryDailySampleDate());
verify(client,times(1)).inventoryFlow(any(),anyString(),anyString(),anyString(),any(),any());verify(leases,times(2)).release(lease);
assertThrows(IllegalStateException.class,()->s.probe(new InventoryFlowProbeService.Request("one","W","C","OTHER",q.startTime(),q.endTime()),"operator"));
verify(client,times(1)).inventoryFlow(any(),anyString(),anyString(),anyString(),any(),any());
saved.set(null);when(client.inventoryFlow(any(),anyString(),anyString(),anyString(),any(),any())).thenThrow(new IllegalStateException("PROVIDER_FAILURE"));
assertThrows(IllegalStateException.class,()->s.probe(q,"operator"));assertEquals("DEAD",saved.get().getStatus());
assertThrows(IllegalStateException.class,()->s.probe(q,"operator"));
verify(client,times(2)).inventoryFlow(any(),anyString(),anyString(),anyString(),any(),any());
}
@Test void refusesFutureAndOversizedWindow() {
var now=LocalDateTime.of(2026,10,15,12,0);
InventoryFlowProbeService.validate(new InventoryFlowProbeService.Request("one","W","C","S",now.minusDays(30),now),now);
assertThrows(IllegalStateException.class,()->InventoryFlowProbeService.validate(new InventoryFlowProbeService.Request("one","W","C","S",now.minusDays(32),now),now));
assertThrows(IllegalStateException.class,()->InventoryFlowProbeService.validate(new InventoryFlowProbeService.Request("one","W","C","S",now,now.plusSeconds(1)),now));
}
@Test void refusesPartialPageAndDuplicateEvidence() throws Exception {
var mapper=new ObjectMapper();
assertTrue(InventoryFlowProbeService.validatePage(mapper.readTree("{\"current\":1,\"size\":1000,\"pages\":1,\"total\":1,\"records\":[{\"id\":\"1\"}]}")));
assertThrows(IllegalStateException.class,()->InventoryFlowProbeService.validatePage(mapper.readTree("{\"current\":1,\"size\":1000,\"pages\":1,\"total\":2,\"records\":[{\"id\":\"1\"}]}")));
assertThrows(IllegalStateException.class,()->InventoryFlowProbeService.validatePage(mapper.readTree("{\"current\":1,\"size\":1000,\"pages\":1,\"total\":2,\"records\":[{\"id\":\"1\"},{\"id\":\"1\"}]}")));
var root=mapper.createObjectNode().put("current",1).put("size",1000).put("pages",2).put("total",1001);
var rows=root.putArray("records");for(int i=0;i<1000;i++)rows.addObject().put("id",String.valueOf(i));
assertFalse(InventoryFlowProbeService.validatePage(root));
}
}