feat(fleet): attach scoped evidence to measured trip records
This commit is contained in:
+25
@@ -0,0 +1,25 @@
|
||||
package com.baiyue.lastleg.fleet.reporting;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
class HttpTripOperationsEvidenceGatewayTest {
|
||||
@Test void platformCategoryTripAndFileMustAllMatch() {
|
||||
assertThat(HttpTripOperationsEvidenceGateway.verified(envelope(0,"FLEET_TRIP_OPERATION_EVIDENCE","71",81,"application/pdf",8),71,81L).fileId()).isEqualTo("81");
|
||||
for(var value:java.util.List.of(envelope(9,"FLEET_TRIP_OPERATION_EVIDENCE","71",81,"image/png",8),
|
||||
envelope(0,"FLEET_VEHICLE_RECORD_PHOTO","71",81,"image/png",8),envelope(0,"FLEET_TRIP_OPERATION_EVIDENCE","72",81,"image/png",8),
|
||||
envelope(0,"FLEET_TRIP_OPERATION_EVIDENCE","71",82,"image/png",8),envelope(0,"FLEET_TRIP_OPERATION_EVIDENCE","71",81,"text/html",8),
|
||||
envelope(0,"FLEET_TRIP_OPERATION_EVIDENCE","71",81,"image/png",10485761)))
|
||||
assertThatThrownBy(()->HttpTripOperationsEvidenceGateway.verified(value,71,81L)).hasMessageContaining("不属于");
|
||||
}
|
||||
@Test void contentRejectsEnvelopesAndEmptyFilesButAcceptsPdfAndImages() {
|
||||
assertThatThrownBy(()->HttpTripOperationsEvidenceGateway.imageContent(ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body("{}".getBytes()))).hasMessageContaining("不可用");
|
||||
assertThatThrownBy(()->HttpTripOperationsEvidenceGateway.imageContent(ResponseEntity.ok().contentType(MediaType.IMAGE_PNG).body(new byte[0]))).hasMessageContaining("不可用");
|
||||
for(var type:java.util.List.of(MediaType.APPLICATION_PDF,MediaType.IMAGE_PNG,MediaType.IMAGE_JPEG))
|
||||
assertThat(HttpTripOperationsEvidenceGateway.imageContent(ResponseEntity.ok().contentType(type).body(new byte[]{1}))).containsExactly(1);
|
||||
}
|
||||
private HttpTripOperationsEvidenceGateway.Envelope envelope(long owner,String type,String trip,long file,String mime,long size){
|
||||
return new HttpTripOperationsEvidenceGateway.Envelope("200",new HttpTripOperationsEvidenceGateway.Metadata(file,owner,type,trip,"meter.pdf",mime,size));
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.baiyue.lastleg.fleet.reporting;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.*;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import java.util.List;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
class TripOperationsRecordAuthorizationTest {
|
||||
@Test void evidenceReadsAndWritesUseSeparateOperationsPermissions() {
|
||||
try(var context=new AnnotationConfigApplicationContext(Config.class)) {
|
||||
var controller=context.getBean(PlatformTripOperationsRecordController.class);
|
||||
var service=context.getBean(TripOperationsRecordService.class);
|
||||
when(service.download("TR-A",81)).thenReturn(new TripOperationsRecordService.Download(new TripOperationsRecordModels.EvidenceFile("81","meter.pdf","application/pdf",1),new byte[]{1}));
|
||||
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken("reader","unused",List.of(new SimpleGrantedAuthority("platform:fleet:operations:list"))));
|
||||
assertThat(controller.download("TR-A",81).getHeaders().getCacheControl()).isEqualTo("no-store");
|
||||
assertThatThrownBy(()->controller.upload(null,"TR-A",null)).isInstanceOf(AccessDeniedException.class);
|
||||
assertThatThrownBy(()->controller.save(null,"TR-A",null)).isInstanceOf(AccessDeniedException.class);
|
||||
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken("writer","unused",List.of(new SimpleGrantedAuthority("platform:fleet:operations:edit"))));
|
||||
assertThatThrownBy(()->controller.download("TR-A",81)).isInstanceOf(AccessDeniedException.class);
|
||||
assertThatThrownBy(()->controller.upload(null,"TR-A",null)).hasMessageContaining("仅平台人员");
|
||||
verify(service).download("TR-A",81);verifyNoMoreInteractions(service);
|
||||
} finally { SecurityContextHolder.clearContext(); }
|
||||
}
|
||||
@Configuration @EnableMethodSecurity
|
||||
static class Config {
|
||||
@Bean TripOperationsRecordService service(){return mock(TripOperationsRecordService.class);}
|
||||
@Bean PlatformTripOperationsRecordController controller(TripOperationsRecordService service){return new PlatformTripOperationsRecordController(service);}
|
||||
}
|
||||
}
|
||||
+45
-2
@@ -17,16 +17,20 @@ import static org.mockito.Mockito.*;
|
||||
|
||||
class TripOperationsRecordPersistenceTest {
|
||||
private JdbcTemplate jdbc;private TripOperationsRecordRepository repository;private TripOperationsRecordService service;
|
||||
private com.baiyue.lastleg.fleet.trip.FleetTripMapper trips;
|
||||
private TripOperationsEvidenceGateway files;
|
||||
@BeforeEach void setup() throws Exception {
|
||||
var source=new DriverManagerDataSource("jdbc:h2:mem:trip-measured-"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1","sa","");jdbc=new JdbcTemplate(source);
|
||||
jdbc.execute("CREATE TABLE ll_fleet_trip(trip_no VARCHAR(64) PRIMARY KEY,status VARCHAR(32),departed_at DATETIME)");
|
||||
jdbc.update("INSERT INTO ll_fleet_trip VALUES('TR-A','IN_TRANSIT','2026-10-01 00:00:00'),('TR-B','PLANNED',NULL)");
|
||||
var match=Pattern.compile("(?s)CREATE TABLE ll_fleet_trip_operations_record \\(.*?;").matcher(Files.readString(Path.of("src/main/resources/db/lastleg.sql")));
|
||||
assertThat(match.find()).as("总控集成实测记录表").isTrue();jdbc.execute(match.group().replace(" JSON"," LONGTEXT"));
|
||||
repository=spy(new TripOperationsRecordRepository(jdbc));var target=new TripOperationsRecordService(repository,new ObjectMapper().registerModule(new JavaTimeModule()));
|
||||
trips=mock(com.baiyue.lastleg.fleet.trip.FleetTripMapper.class);files=mock(TripOperationsEvidenceGateway.class);
|
||||
var trip=new com.baiyue.lastleg.fleet.trip.FleetTripEntity();trip.setId(71L);trip.setTripNo("TR-A");trip.setDepartedAt(java.time.LocalDateTime.of(2026,10,1,0,0));when(trips.findByNo("TR-A")).thenReturn(trip);
|
||||
repository=spy(new TripOperationsRecordRepository(jdbc));var target=new TripOperationsRecordService(repository,new ObjectMapper().registerModule(new JavaTimeModule()),trips,files);
|
||||
var proxy=new ProxyFactory(target);proxy.setProxyTargetClass(true);proxy.addAdvice(new TransactionInterceptor(new DataSourceTransactionManager(source),new AnnotationTransactionAttributeSource()));service=(TripOperationsRecordService)proxy.getProxy();
|
||||
}
|
||||
private TripOperationsRecordModels.Save request(long version,String key,BigDecimal distance,BigDecimal fuel){return new TripOperationsRecordModels.Save(version,key,new TripOperationsRecordModels.Details(distance,fuel,null),null);}
|
||||
private TripOperationsRecordModels.Save request(long version,String key,BigDecimal distance,BigDecimal fuel){return new TripOperationsRecordModels.Save(version,key,new TripOperationsRecordModels.Input(distance,fuel,null,java.util.List.of()),null);}
|
||||
@Test void onlyDepartedTripsCanBeMeasuredAndMissingValuesAreNotZero() {
|
||||
assertThat(service.get("TR-A").details().actualDistanceMiles()).isNull();assertThat(service.get("TR-A").recordVersion()).isZero();
|
||||
assertThatThrownBy(()->service.save("TR-B","1",request(0,"before-depart",BigDecimal.ONE,null))).hasMessageContaining("实际已发车");
|
||||
@@ -53,4 +57,43 @@ class TripOperationsRecordPersistenceTest {
|
||||
assertThatThrownBy(()->service.save("TR-A","1",request(0,"failure-key",BigDecimal.ONE,null))).hasMessageContaining("audit-failure");
|
||||
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM ll_fleet_trip_operations_record",Integer.class)).isZero();
|
||||
}
|
||||
private TripOperationsRecordModels.Save evidence(long version,String key,Long... ids){
|
||||
return new TripOperationsRecordModels.Save(version,key,new TripOperationsRecordModels.Input(null,null,null,java.util.List.of(ids)),null);
|
||||
}
|
||||
@Test void optionalEvidenceIsValidatedAndFrozenWithHistoryAndExactReplay() {
|
||||
var file=new TripOperationsRecordModels.EvidenceFile("81","meter.pdf","application/pdf",8);
|
||||
when(files.metadata(71,81)).thenReturn(file);
|
||||
var command=evidence(0,"attach-proof",81L);var saved=service.save("TR-A","1",command);
|
||||
assertThat(saved.details().evidenceFiles()).containsExactly(file);assertThat(saved.recordVersion()).isEqualTo(1);
|
||||
service.save("TR-A","1",evidence(1,"clear-proof"));
|
||||
assertThat(service.get("TR-A").details().evidenceFiles()).isEmpty();
|
||||
reset(files);
|
||||
assertThat(service.save("TR-A","1",command).details().evidenceFiles()).containsExactly(file);
|
||||
verifyNoInteractions(files);
|
||||
assertThat(service.history("TR-A",1,20).records().get(1).details().evidenceFiles()).containsExactly(file);
|
||||
assertThatThrownBy(()->service.save("TR-A","1",evidence(0,"attach-proof",82L))).hasMessageContaining("其他实测内容");
|
||||
assertThatThrownBy(()->service.save("TR-A","1",evidence(1,"stale-proof",81L))).hasMessageContaining("已变化");
|
||||
}
|
||||
@Test void duplicateOrForeignEvidenceCannotCreateRecordAndEmptyEvidenceIsLegal() {
|
||||
assertThat(service.save("TR-A","1",evidence(0,"empty-evidence")).recordVersion()).isZero();
|
||||
assertThatThrownBy(()->service.save("TR-A","1",evidence(0,"duplicate-evidence",81L,81L))).hasMessageContaining("不同");
|
||||
when(files.metadata(71,81)).thenThrow(new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.BAD_REQUEST,"文件不属于当前车次"));
|
||||
assertThatThrownBy(()->service.save("TR-A","1",evidence(0,"foreign-evidence",81L))).hasMessageContaining("不属于");
|
||||
assertThat(repository.count("TR-A")).isZero();
|
||||
}
|
||||
@Test void uploadDoesNotCreateMeasuredRecordAndHistoricalDownloadRevalidatesOwnership() {
|
||||
var blob=new org.springframework.mock.web.MockMultipartFile("file","meter.pdf","application/pdf",new byte[]{1});
|
||||
var file=new TripOperationsRecordModels.EvidenceFile("81","meter.pdf","application/pdf",1);
|
||||
when(files.upload(71,"1",blob)).thenReturn(file);
|
||||
assertThat(service.upload("TR-A","1",blob)).isEqualTo(file);assertThat(repository.count("TR-A")).isZero();
|
||||
when(files.upload(71,"1",blob)).thenThrow(new IllegalStateException("upload interrupted"));
|
||||
assertThatThrownBy(()->service.upload("TR-A","1",blob)).hasMessageContaining("interrupted");assertThat(repository.count("TR-A")).isZero();
|
||||
assertThatThrownBy(()->service.download("TR-A",81)).hasMessageContaining("尚未保存");
|
||||
when(files.metadata(71,81)).thenReturn(file);when(files.download(71,81)).thenReturn(new byte[]{1});
|
||||
service.save("TR-A","1",evidence(0,"bind-for-read",81L));
|
||||
service.save("TR-A","1",evidence(1,"remove-reference"));
|
||||
assertThat(service.download("TR-A",81).content()).containsExactly(1);
|
||||
assertThatThrownBy(()->service.download("TR-A",82)).hasMessageContaining("尚未保存");
|
||||
assertThatThrownBy(()->service.upload("TR-B","1",blob)).hasMessageContaining("实际已发车");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user