feat(selfop): add bounded inventory flow diagnostic through account egress
This commit is contained in:
+22
@@ -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());
|
||||
}
|
||||
}
|
||||
+15
@@ -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");
|
||||
|
||||
+55
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user