package com.battery.business.service.impl; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; import static org.mockito.ArgumentMatchers.*; import java.math.BigDecimal; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; import com.alibaba.fastjson2.JSON; import com.battery.business.domain.*; import com.battery.business.mapper.TTestMapper; import com.ruoyi.common.exception.ServiceException; /** 修订入口必须保留数据库中的修改前事实,并拒绝越界身份和字段。 */ class TestConsoleDataRevisionQueryServiceTest { private final TTestMapper mapper = mock(TTestMapper.class); private final TestConsoleDataServiceImpl service = new TestConsoleDataServiceImpl(mapper); @org.junit.jupiter.api.BeforeEach void generatedRevisionId() { when(mapper.insertDataRevisionAction(any())).thenAnswer(call -> { ((TTestDataRevisionAction) call.getArgument(0)).setActionId(1L); return 1; }); } @Test void savesDatabaseBeforeContentInsteadOfClientClaims() { var action = action("EDIT", "realtime:7"); when(mapper.selectDataRecord(1L, "REALTIME", 7L)).thenReturn(Map.of("voltage_v", new BigDecimal("3.7"), "source_identity", "cabinet-a")); var item = new DataRevisionItem(); item.setStableIdentity("realtime:7"); item.setFieldValues("{\"voltage\":4.125}"); service.submitAction(action, List.of(item)); var before = JSON.parseArray(action.getBeforeContentJson()).getJSONObject(0); assertEquals("cabinet-a", before.getString("source_identity")); assertEquals(new BigDecimal("3.7"), before.getBigDecimal("voltage_v")); var order = inOrder(mapper); order.verify(mapper).insertDataRevisionAction(action); order.verify(mapper).updateDataRecord(1L, "REALTIME", 7L, Map.of("voltage_v", new BigDecimal("4.125"))); } @Test void deletionPersistsCompleteContentBeforeRemovingRecord() { var action = action("DELETE", "realtime:7"); when(mapper.selectDataRecord(1L, "REALTIME", 7L)).thenReturn(Map.of("cabinet_device_time_raw", "2026-09-05 12:00:00", "voltage_v", 3.7)); service.submitAction(action, List.of()); assertTrue(action.getBeforeContentJson().contains("cabinet_device_time_raw")); var order = inOrder(mapper); order.verify(mapper).insertDataRevisionAction(action); order.verify(mapper).deleteDataRecord(1L, "REALTIME", 7L); } @Test void stepExtensionEditPreservesOtherMetricsAndDisplaysPreviousValue() { var action = action("EDIT", "step:7"); action.setDataType("STEP"); when(mapper.selectDataRecord(1L, "STEP", 7L)).thenReturn(Map.of("extension_measurements_json", "{\"startVoltage\":3.2,\"stepDuration\":60}")); var item = new DataRevisionItem(); item.setStableIdentity("step:7"); item.setFieldValues("{\"startVoltage\":3.4}"); service.submitAction(action, List.of(item)); var change = JSON.parseArray(action.getChangesSummary()).getJSONObject(0); assertEquals(new BigDecimal("3.2"), change.getJSONObject("before").getBigDecimal("startVoltage")); var values = org.mockito.ArgumentCaptor.forClass(Map.class); verify(mapper).updateDataRecord(eq(1L), eq("STEP"), eq(7L), values.capture()); var extension = JSON.parseObject(values.getValue().get("extension_measurements_json").toString()); assertEquals(60, extension.getIntValue("stepDuration")); assertEquals(new BigDecimal("3.4"), extension.getBigDecimal("startVoltage")); } @Test void rejectsWrongDataTypeIdentityAndOtherTestsRecords() { assertThrows(ServiceException.class, () -> service.submitAction(action("DELETE", "step:7"), List.of())); assertThrows(ServiceException.class, () -> service.submitAction(action("DELETE", "realtime:8"), List.of())); verify(mapper, never()).insertDataRevisionAction(any()); } @Test void medianEditAuditsPreviousValuesAndPreservesLegacyFacts() { var action = action("EDIT", "step:7"); action.setDataType("STEP"); String previous = "{\"chargeMedianVoltage\":0,\"dischargeMedianVoltage\":1.1709,\"chargePeakVoltage\":1.8,\"dischargeValleyVoltage\":0.8}"; when(mapper.selectDataRecord(1L, "STEP", 7L)).thenReturn(Map.of("extension_measurements_json", previous)); var item = new DataRevisionItem(); item.setStableIdentity("step:7"); item.setFieldValues("{\"chargeMedianVoltage\":1.6431,\"dischargeMedianVoltage\":null}"); service.submitAction(action, List.of(item)); assertEquals(previous, JSON.parseArray(action.getBeforeContentJson()).getJSONObject(0).getString("extension_measurements_json")); var before = JSON.parseArray(action.getChangesSummary()).getJSONObject(0).getJSONObject("before"); assertEquals(new BigDecimal("1.1709"), before.getBigDecimal("dischargeMedianVoltage")); var values = org.mockito.ArgumentCaptor.forClass(Map.class); verify(mapper).updateDataRecord(eq(1L), eq("STEP"), eq(7L), values.capture()); var extension = JSON.parseObject(values.getValue().get("extension_measurements_json").toString()); assertEquals(new BigDecimal("1.6431"), extension.getBigDecimal("chargeMedianVoltage")); assertNull(extension.get("dischargeMedianVoltage")); assertEquals(new BigDecimal("1.8"), extension.getBigDecimal("chargePeakVoltage")); assertEquals(new BigDecimal("0.8"), extension.getBigDecimal("dischargeValleyVoltage")); } @Test void rejectsIdentityEditsAndNonNumericMeasurements() { when(mapper.selectDataRecord(1L, "REALTIME", 7L)).thenReturn(Map.of("voltage_v", 3.7)); for (String fields : List.of("{\"test_id\":2}", "{\"voltage\":\"invalid\"}")) { var item = new DataRevisionItem(); item.setStableIdentity("realtime:7"); item.setFieldValues(fields); assertThrows(ServiceException.class, () -> service.submitAction(action("EDIT", "realtime:7"), List.of(item))); } verify(mapper, never()).insertDataRevisionAction(any()); } @Test void importRequiresCompletedTest() { assertThrows(ServiceException.class, () -> service.submitAction(action("IMPORT", "manual-1"), List.of())); verify(mapper, never()).insertImportedDataRecord(anyString(), any()); } @Test void completedRealtimeImportKeepsSourceRecordAndSequence() { var action = action("IMPORT", "manual-1"); mapper.selectTestByIdForUpdate(1L).setTestStatus("COMPLETED"); when(mapper.selectImportBinding(1L, "BAT-1")).thenReturn(Map.of("test_battery_id", 4L, "binding_id", 5L, "adapter_code", "adapter-a", "devtype", 27, "devid", 1, "subdevid", 1, "chlid", 1)); action.setChangesSummary("[{\"recordNo\":\"manual-1\",\"batteryNo\":\"BAT-1\",\"recordedAt\":1770000000123,\"cycleNo\":1,\"stepNo\":1,\"stepName\":\"恒流充电\",\"sequenceId\":17,\"voltage\":3.6}]"); service.submitAction(action, List.of()); var values = org.mockito.ArgumentCaptor.forClass(Map.class); verify(mapper).insertImportedDataRecord(eq("REALTIME"), values.capture()); assertEquals("manual-1", values.getValue().get("source_record_id")); assertEquals(17L, values.getValue().get("cabinet_sequence_id")); assertEquals("adapter-a:27:1:1:1", values.getValue().get("source_identity")); } private TTestDataRevisionAction action(String operation, String identity) { TTest test = new TTest(); test.setTestId(1L); test.setTestStatus("RUNNING"); when(mapper.selectTestByIdForUpdate(1L)).thenReturn(test); var action = new TTestDataRevisionAction(); action.setTestId(1L); action.setDataType("REALTIME"); action.setOperationType(operation); action.setIdentitySummary(JSON.toJSONString(List.of(identity))); action.setScopeSummary("核对一条数据"); action.setReason("现场核对"); action.setOperatorName("operator"); action.setChangesSummary("[]"); return action; } }