diff --git a/.gitignore b/.gitignore index d731f987..5a8cbbce 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,9 @@ build/ /sdkLog/ /tmp/ +# 测试源码仅在本地保留,不提交到仓库 +/src/test/ + # 本地设计过程文档不纳入版本库 /docs/superpowers/ diff --git a/src/test/java/com/inspect/nvr/controller/IvsServerControllerTest.java b/src/test/java/com/inspect/nvr/controller/IvsServerControllerTest.java deleted file mode 100644 index f85f172d..00000000 --- a/src/test/java/com/inspect/nvr/controller/IvsServerControllerTest.java +++ /dev/null @@ -1,112 +0,0 @@ -package com.inspect.nvr.controller; - -import com.inspect.nvr.ivs.service.IvsCameraSyncService; -import com.inspect.nvr.ivs.service.IvsOpenApiService; -import com.inspect.nvr.view.IvsPlatformSnapshotView; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; - -import java.util.Collections; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; - -/** - * 验证 IVS 抓图列表接口返回的图片地址格式。 - */ -@RunWith(MockitoJUnitRunner.class) -public class IvsServerControllerTest { - - @Mock - private IvsCameraSyncService ivsCameraSyncService; - - @Mock - private IvsOpenApiService ivsOpenApiService; - - private MockMvc mockMvc; - - @Before - public void setUp() { - // 使用独立控制器测试,避免启动数据库和厂商 SDK 环境。 - IvsServerController controller = new IvsServerController( - ivsCameraSyncService, ivsOpenApiService); - mockMvc = standaloneSetup(controller).build(); - } - - @Test - public void expandsRelativeSnapshotUrlsToCurrentRequestAddress() throws Exception { - // 模拟适配器返回 IVS 文档中的相对抓图地址。 - IvsPlatformSnapshotView.SnapshotInfo info = IvsPlatformSnapshotView.SnapshotInfo - .builder() - .previewUrl("/platform/snapshot/task-001") - .pictureUrl("/platform/snapshot/task-001") - .build(); - IvsPlatformSnapshotView response = IvsPlatformSnapshotView.builder() - .resultCode("0") - .snapshotInfoList(IvsPlatformSnapshotView.SnapshotInfoList.builder() - .total(1) - .snapshotInfos(Collections.singletonList(info)) - .build()) - .build(); - when(ivsOpenApiService.snapshotList(any())).thenReturn(response); - - // 设置与用户调用示例一致的协议、主机和 8081 端口。 - mockMvc.perform(post("/platform/snapshotList") - .with(request -> { - request.setScheme("http"); - request.setServerName("127.0.0.1"); - request.setServerPort(8081); - return request; - }) - .contentType(MediaType.APPLICATION_JSON) - .content("{\"taskID\":\"task-001\"}")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.snapshotInfoList.snapshotInfos[0].previewUrl") - .value("http://127.0.0.1:8081/platform/snapshot/task-001")) - .andExpect(jsonPath("$.snapshotInfoList.snapshotInfos[0].pictureUrl") - .value("http://127.0.0.1:8081/platform/snapshot/task-001")); - } - - @Test - public void keepsExistingAbsoluteSnapshotUrls() throws Exception { - // 已经是绝对地址时不应再次拼接当前服务地址。 - IvsPlatformSnapshotView.SnapshotInfo info = IvsPlatformSnapshotView.SnapshotInfo - .builder() - .previewUrl("https://cdn.example.com/task-001.jpg") - .pictureUrl("https://cdn.example.com/task-001.jpg") - .build(); - IvsPlatformSnapshotView response = IvsPlatformSnapshotView.builder() - .resultCode("0") - .snapshotInfoList(IvsPlatformSnapshotView.SnapshotInfoList.builder() - .total(1) - .snapshotInfos(Collections.singletonList(info)) - .build()) - .build(); - when(ivsOpenApiService.snapshotList(any())).thenReturn(response); - - // 通过另一个主机调用,确认外部绝对地址仍保持原样。 - mockMvc.perform(post("/platform/snapshotList") - .with(request -> { - request.setScheme("http"); - request.setServerName("192.168.1.10"); - request.setServerPort(8081); - return request; - }) - .contentType(MediaType.APPLICATION_JSON) - .content("{\"taskID\":\"task-001\"}")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.snapshotInfoList.snapshotInfos[0].previewUrl") - .value("https://cdn.example.com/task-001.jpg")) - .andExpect(jsonPath("$.snapshotInfoList.snapshotInfos[0].pictureUrl") - .value("https://cdn.example.com/task-001.jpg")); - } -} diff --git a/src/test/java/com/inspect/nvr/dahua/DahuaIvsVendorServiceTest.java b/src/test/java/com/inspect/nvr/dahua/DahuaIvsVendorServiceTest.java deleted file mode 100644 index b1e97597..00000000 --- a/src/test/java/com/inspect/nvr/dahua/DahuaIvsVendorServiceTest.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.inspect.nvr.dahua; - -import com.inspect.nvr.daHuaCarme.jna.NetSDKLib; -import com.inspect.nvr.domain.Infrared.NvrInfo; -import com.inspect.nvr.domain.device.IvsPresetListView; -import com.inspect.nvr.hik.domain.HikNvrCameraTarget; -import com.inspect.nvr.hik.service.HikNvrCameraTargetService; -import com.inspect.nvr.service.DahuaLoginService; -import com.sun.jna.ptr.IntByReference; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.test.util.ReflectionTestUtils; - -import java.nio.charset.StandardCharsets; - -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.when; - -/** 验证大华 IVS 适配器按真实固件 JSON 读取多通道预置位。 */ -@RunWith(MockitoJUnitRunner.class) -public class DahuaIvsVendorServiceTest { - - /** 模拟大华原生 SDK。 */ - @Mock - private NetSDKLib sdk; - /** 模拟大华登录缓存。 */ - @Mock - private DahuaLoginService loginService; - /** 模拟完整 cameraCode 目标解析。 */ - @Mock - private HikNvrCameraTargetService cameraTargetService; - /** 待验证的大华厂商适配器。 */ - private DahuaIvsVendorService service; - - /** 注入最小依赖并固定目标为第三路大华摄像机。 */ - @Before - public void setUp() { - service = new DahuaIvsVendorService(); - ReflectionTestUtils.setField(service, "sdk", sdk); - ReflectionTestUtils.setField(service, "loginService", loginService); - ReflectionTestUtils.setField(service, "cameraTargetService", cameraTargetService); - NvrInfo nvr = new NvrInfo(); - nvr.setNvrCompany("DAHUA"); - nvr.setNvrIp("192.168.1.134"); - HikNvrCameraTarget target = new HikNvrCameraTarget( - "32078710632508667003#domain-a", 3, nvr); - when(cameraTargetService.resolve(anyString())).thenReturn(target); - when(loginService.login(nvr)).thenReturn(new NetSDKLib.LLong(88)); - } - - /** SDK 返回全部通道嵌套 table 时只能读取第三路启用的预置位。 */ - @Test - public void readsEnabledPresetsFromTargetChannelJson() { - String json = "{\"id\":1,\"params\":{\"table\":[[],[],[" - + "{\"Enable\":true,\"Index\":1,\"Name\":\"预置位1\",\"Position\":[0.0,0.0,0.0]}," - + "{\"Enable\":false,\"Index\":2,\"Name\":\"Preset2\",\"Position\":[0.0,0.0,0.0]}," - + "{\"Enable\":true,\"Index\":29,\"Name\":\"Gate\",\"Position\":[1.0,2.0,3.0]}" - + "]]}}"; - // 模拟 CLIENT_GetNewDevConfig 把真实固件 JSON 写入调用方缓冲区。 - when(sdk.CLIENT_GetNewDevConfig( - any(), eq(NetSDKLib.CFG_CMD_PTZ_PRESET), eq(-1), - any(byte[].class), anyInt(), any(IntByReference.class), - anyInt(), isNull())).thenAnswer(invocation -> { - byte[] buffer = invocation.getArgument(3); - byte[] source = json.getBytes(StandardCharsets.UTF_8); - System.arraycopy(source, 0, buffer, 0, source.length); - return true; - }); - - IvsPresetListView result = service.presetList( - "32078710632508667003", "domain-a"); - - assertEquals(2, result.getPtzPresetNum()); - assertEquals(1, result.getPtzPresetInfoList().getPtzPresetInfo().get(0).getPresetIndex()); - assertEquals("预置位1", result.getPtzPresetInfoList().getPtzPresetInfo().get(0).getPresetName()); - assertEquals(29, result.getPtzPresetInfoList().getPtzPresetInfo().get(1).getPresetIndex()); - assertEquals("Gate", result.getPtzPresetInfoList().getPtzPresetInfo().get(1).getPresetName()); - } -} diff --git a/src/test/java/com/inspect/nvr/dahua/DahuaNvrChannelClientTest.java b/src/test/java/com/inspect/nvr/dahua/DahuaNvrChannelClientTest.java deleted file mode 100644 index 647e816b..00000000 --- a/src/test/java/com/inspect/nvr/dahua/DahuaNvrChannelClientTest.java +++ /dev/null @@ -1,156 +0,0 @@ -package com.inspect.nvr.dahua; - -import com.inspect.nvr.daHuaCarme.jna.NetSDKLib; -import com.inspect.nvr.daHuaCarme.jna.dahua.ToolKits; -import com.inspect.nvr.ivs.pojo.IvsNvr; -import com.inspect.nvr.ivs.util.IvsNvrChannelClient; -import com.inspect.nvr.service.DahuaLoginService; -import com.sun.jna.Pointer; -import com.sun.jna.ptr.IntByReference; - -import org.junit.Test; -import org.springframework.test.util.ReflectionTestUtils; - -import java.nio.charset.Charset; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * 验证大华 NetSDK RemoteDevice 配置和在线状态能够转换为 IVS 同步通道。 - */ -public class DahuaNvrChannelClientTest { - - /** 大华设备文本测试使用与生产代码一致的 GBK 编码。 */ - private static final Charset DAHUA_CHARSET = Charset.forName("GBK"); - - /** - * NetSDK 的零基统一通道、远程 IP、名称和在线状态必须映射为一基同步通道。 - */ - @Test - public void mapsAllConfiguredRemoteCamerasAndConnectionStates() { - NetSDKLib sdk = mock(NetSDKLib.class); - NetSDKLib configSdk = mock(NetSDKLib.class); - DahuaLoginService loginService = mock(DahuaLoginService.class); - NetSDKLib.LLong loginHandle = new NetSDKLib.LLong(19); - when(loginService.login(any())).thenReturn(loginHandle); - - // 模拟 NetSDK 成功返回 RemoteDevice 原始配置。 - when(sdk.CLIENT_GetNewDevConfig( - eq(loginHandle), - eq(NetSDKLib.CFG_CMD_REMOTEDEVICE), - eq(-1), - any(byte[].class), - anyInt(), - any(IntByReference.class), - anyInt(), - isNull())).thenAnswer(invocation -> { - put(invocation.getArgument(3), "{\"params\":{\"table\":{}}}"); - return true; - }); - - // 模拟 dhconfigsdk 将原始 JSON 解析成两路连续 RemoteDevice 结构体。 - when(configSdk.CLIENT_ParseData( - eq(NetSDKLib.CFG_CMD_REMOTEDEVICE), - any(byte[].class), - any(Pointer.class), - anyInt(), - isNull())).thenAnswer(invocation -> { - Pointer output = invocation.getArgument(2); - NetSDKLib.AV_CFG_RemoteDevice first = remoteDevice( - 0, "192.168.1.220", "东门球机", 37777); - NetSDKLib.AV_CFG_RemoteDevice second = remoteDevice( - 1, "192.168.1.221", "西门枪机", 8000); - ToolKits.SetStructDataToPointer(first, output, 0); - ToolKits.SetStructDataToPointer(second, output, first.size()); - return true; - }); - - // 模拟 QueryDevInfo 返回第一路已连接、第二路未连接。 - when(sdk.CLIENT_QueryDevInfo( - eq(loginHandle), - eq(NetSDKLib.NET_QUERY_GET_CAMERA_STATE), - any(Pointer.class), - any(Pointer.class), - any(), - anyInt())).thenAnswer(invocation -> { - Pointer outputPointer = invocation.getArgument(3); - NetSDKLib.NET_OUT_GET_CAMERA_STATEINFO output = - new NetSDKLib.NET_OUT_GET_CAMERA_STATEINFO(); - ToolKits.GetPointerData(outputPointer, output); - NetSDKLib.NET_CAMERA_STATE_INFO first = new NetSDKLib.NET_CAMERA_STATE_INFO(); - first.nChannel = 0; - first.emConnectionState = - NetSDKLib.EM_CAMERA_STATE_TYPE.EM_CAMERA_STATE_TYPE_CONNECTED; - ToolKits.SetStructDataToPointer(first, output.pCameraStateInfo, 0); - NetSDKLib.NET_CAMERA_STATE_INFO second = new NetSDKLib.NET_CAMERA_STATE_INFO(); - second.nChannel = 1; - second.emConnectionState = - NetSDKLib.EM_CAMERA_STATE_TYPE.EM_CAMERA_STATE_TYPE_UNCONNECT; - ToolKits.SetStructDataToPointer( - second, output.pCameraStateInfo, first.size()); - output.nValidNum = 2; - ToolKits.SetStructDataToPointer(output, outputPointer, 0); - return true; - }); - - DahuaNvrChannelClient client = new DahuaNvrChannelClient(); - ReflectionTestUtils.setField(client, "dhNetSDK", sdk); - ReflectionTestUtils.setField(client, "dhConfigSDK", configSdk); - ReflectionTestUtils.setField(client, "dahuaLoginService", loginService); - - // 调用实际转换链路,确保数据库参数经登录和两次 SDK 查询后生成统一 Channel。 - List channels = client.listChannels(nvr()); - - assertEquals(2, channels.size()); - IvsNvrChannelClient.Channel first = channels.get(0); - assertEquals(Integer.valueOf(1), first.getId()); - assertEquals("东门球机", first.getName()); - assertEquals("DAHUA", first.getProtocol()); - assertEquals("192.168.1.220", first.getIp()); - assertEquals(Integer.valueOf(37777), first.getPort()); - assertEquals(Integer.valueOf(1), first.getSourceChannel()); - assertTrue(first.isOnline()); - IvsNvrChannelClient.Channel second = channels.get(1); - assertEquals(Integer.valueOf(2), second.getId()); - assertEquals("西门枪机", second.getName()); - assertFalse(second.isOnline()); - } - - /** 构造大华同步所需的最小数据库 NVR 记录。 */ - private IvsNvr nvr() { - IvsNvr nvr = new IvsNvr(); - nvr.setNvrIp("192.168.1.134"); - nvr.setSdkPort(37777); - nvr.setUsername("admin"); - nvr.setPassword("password"); - nvr.setVendor("Dahua"); - return nvr; - } - - /** 构造 config SDK 返回的最小远程设备结构体。 */ - private static NetSDKLib.AV_CFG_RemoteDevice remoteDevice( - int channel, String ip, String name, int port) { - NetSDKLib.AV_CFG_RemoteDevice device = new NetSDKLib.AV_CFG_RemoteDevice(); - device.bEnable = 1; - put(device.szID, "uuid:System_CONFIG_NETCAMERA_INFO_" + channel); - put(device.szIP, ip); - put(device.szName, name); - device.nPort = port; - return device; - } - - /** 将测试文本按大华设备编码写入固定长度的零结尾字节数组。 */ - private static void put(byte[] target, String value) { - byte[] bytes = value.getBytes(DAHUA_CHARSET); - System.arraycopy(bytes, 0, target, 0, Math.min(bytes.length, target.length - 1)); - } -} diff --git a/src/test/java/com/inspect/nvr/dahua/DahuaNvrPlaybackServiceTest.java b/src/test/java/com/inspect/nvr/dahua/DahuaNvrPlaybackServiceTest.java deleted file mode 100644 index aace0914..00000000 --- a/src/test/java/com/inspect/nvr/dahua/DahuaNvrPlaybackServiceTest.java +++ /dev/null @@ -1,148 +0,0 @@ -package com.inspect.nvr.dahua; - -import com.inspect.nvr.daHuaCarme.jna.NetSDKLib; -import com.inspect.nvr.daHuaCarme.jna.dahua.LastError; -import com.inspect.nvr.hik.domain.HikNvrPlaybackRequest; -import com.inspect.nvr.hik.domain.HikNvrPlaybackResponse; -import com.inspect.nvr.service.DahuaLoginService; -import com.sun.jna.ptr.IntByReference; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.test.util.ReflectionTestUtils; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** 大华录像查询的最小 SDK 映射回归测试。 */ -@RunWith(MockitoJUnitRunner.class) -public class DahuaNvrPlaybackServiceTest { - - /** 大华原生 SDK 模拟对象。 */ - @Mock - private NetSDKLib sdk; - /** 大华登录服务模拟对象。 */ - @Mock - private DahuaLoginService loginService; - /** 待验证的大华录像服务。 */ - private DahuaNvrPlaybackService service; - - /** 注入模拟依赖并准备固定登录句柄。 */ - @Before - public void setUp() { - service = new DahuaNvrPlaybackService(); - ReflectionTestUtils.setField(service, "sdk", sdk); - ReflectionTestUtils.setField(service, "loginService", loginService); - when(loginService.login(any())).thenReturn(new NetSDKLib.LLong(88)); - when(sdk.CLIENT_SetDeviceMode(any(), anyInt(), any())).thenReturn(true); - } - - /** - * 设备返回相邻录像文件时应合并成一个连续区间,并生成大华 RTSP 地址。 - */ - @Test - public void mergesAdjacentRecordFilesAndBuildsDahuaPlaybackUrl() { - // 模拟大华 SDK 返回两段首尾相接的主码流录像。 - doAnswer(invocation -> { - NetSDKLib.NET_RECORDFILE_INFO[] files = invocation.getArgument(6); - setTime(files[0].starttime, 10, 0, 0); - setTime(files[0].endtime, 10, 5, 0); - setTime(files[1].starttime, 10, 5, 0); - setTime(files[1].endtime, 10, 10, 0); - IntByReference count = invocation.getArgument(8); - count.setValue(2); - return true; - }).when(sdk).CLIENT_QueryRecordFile( - any(), - anyInt(), - anyInt(), - any(NetSDKLib.NET_TIME.class), - any(NetSDKLib.NET_TIME.class), - isNull(), - any(NetSDKLib.NET_RECORDFILE_INFO[].class), - anyInt(), - any(IntByReference.class), - anyInt(), - anyBoolean()); - - HikNvrPlaybackResponse response = service.search(request()); - - // 相邻文件必须合并,避免 HLS 创建被误判为多个不连续录像段。 - assertTrue(response.isHasRecord()); - assertEquals(1, response.getRecords().size()); - assertEquals("20260811100000", response.getRecords().get(0).getTimeRange().get(0)); - assertEquals("20260811101000", response.getRecords().get(0).getTimeRange().get(1)); - assertEquals( - "rtsp://admin:p%40ss@192.168.1.108:554/cam/playback" - + "?channel=2&subtype=0&starttime=2026_08_11_10_00_00" - + "&endtime=2026_08_11_10_10_00", - response.getRecords().get(0).getRtspUrl()); - // 页面2号通道必须转换为大华0基SDK通道1。 - verify(sdk).CLIENT_QueryRecordFile( - any(), - org.mockito.ArgumentMatchers.eq(1), - anyInt(), - any(NetSDKLib.NET_TIME.class), - any(NetSDKLib.NET_TIME.class), - isNull(), - any(NetSDKLib.NET_RECORDFILE_INFO[].class), - anyInt(), - any(IntByReference.class), - anyInt(), - anyBoolean()); - } - - /** 大华“未找到录像”错误码属于正常空结果,不能转换成 HTTP 502。 */ - @Test - public void returnsEmptyResultWhenDeviceHasNoRecord() { - // 模拟真实 NVR:查询方法返回 false,同时最后错误码为 NET_NO_RECORD_FOUND。 - when(sdk.CLIENT_QueryRecordFile( - any(), anyInt(), anyInt(), any(NetSDKLib.NET_TIME.class), - any(NetSDKLib.NET_TIME.class), isNull(), - any(NetSDKLib.NET_RECORDFILE_INFO[].class), anyInt(), - any(IntByReference.class), anyInt(), anyBoolean())) - .thenReturn(false); - when(sdk.CLIENT_GetLastError()).thenReturn(LastError.NET_NO_RECORD_FOUND); - - HikNvrPlaybackResponse response = service.search(request()); - - assertFalse(response.isHasRecord()); - assertTrue(response.getRecords().isEmpty()); - } - - /** 创建一条使用数据库大华凭据的录像请求。 */ - private HikNvrPlaybackRequest request() { - return HikNvrPlaybackRequest.builder() - .vendor("DAHUA") - .cameraCode("22078710632508667002#domain-a") - .nvrIp("192.168.1.108") - .sdkPort(37777) - .rtspPort(554) - .channel(2) - .streamType(1) - .username("admin") - .password("p@ss") - .startTime("2026-08-11 10:00:00") - .endTime("2026-08-11 10:10:00") - .build(); - } - - /** 填充固定日期下的大华 SDK 时间结构。 */ - private void setTime( - NetSDKLib.NET_TIME time, - int hour, - int minute, - int second) { - time.setTime(2026, 8, 11, hour, minute, second); - } -} diff --git a/src/test/java/com/inspect/nvr/domain/ivs/IvsSnapshotCommandResponseTest.java b/src/test/java/com/inspect/nvr/domain/ivs/IvsSnapshotCommandResponseTest.java deleted file mode 100644 index 4bee8ebc..00000000 --- a/src/test/java/com/inspect/nvr/domain/ivs/IvsSnapshotCommandResponseTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.inspect.nvr.domain.ivs; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; - -/** - * 校验平台抓图任务创建响应的 IVS 顶层 JSON 结构。 - */ -public class IvsSnapshotCommandResponseTest { - - @Test - public void serializesResultCodeAndTaskIdAtTopLevel() throws Exception { - // 构造一个成功的抓图任务响应,模拟海康适配器返回的任务编号。 - IvsSnapshotCommandResponse response = IvsSnapshotCommandResponse.builder() - .resultCode(0) - .taskID("0BE0B566938F11EC80008CE5EF7F1DD0") - .build(); - - // 使用项目实际使用的 Jackson 序列化响应,验证接口输出而不是 Java 对象字段。 - JsonNode json = new ObjectMapper().readTree( - new ObjectMapper().writeValueAsString(response)); - - // resultCode 必须是顶层数字,taskID 必须是顶层字符串。 - assertEquals(0, json.get("resultCode").asInt()); - assertEquals("0BE0B566938F11EC80008CE5EF7F1DD0", - json.get("taskID").asText()); - // 响应只能包含文档定义的两个顶层字段,不能出现 resultBody 包装层。 - assertFalse(json.has("resultBody")); - assertEquals(2, json.size()); - } -} diff --git a/src/test/java/com/inspect/nvr/hik/controller/HikNvrPlaybackControllerTest.java b/src/test/java/com/inspect/nvr/hik/controller/HikNvrPlaybackControllerTest.java deleted file mode 100644 index 23724eb1..00000000 --- a/src/test/java/com/inspect/nvr/hik/controller/HikNvrPlaybackControllerTest.java +++ /dev/null @@ -1,179 +0,0 @@ -package com.inspect.nvr.hik.controller; - -import com.inspect.nvr.hik.domain.HikNvrPlaybackClipRequest; -import com.inspect.nvr.hik.domain.HikNvrPlaybackRecord; -import com.inspect.nvr.hik.domain.HikNvrPlaybackResponse; -import com.inspect.nvr.hik.exception.HikNvrPlaybackException; -import com.inspect.nvr.hik.service.HikNvrPlaybackClipFile; -import com.inspect.nvr.hik.service.HikNvrPlaybackClipService; -import com.inspect.nvr.hik.service.HikNvrPlaybackService; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; - -import java.io.ByteArrayOutputStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Paths; -import java.time.LocalDateTime; -import java.util.Arrays; -import java.util.Collections; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; - -@RunWith(MockitoJUnitRunner.class) -public class HikNvrPlaybackControllerTest { - - private static final String REQUEST_JSON = "{" - + "\"nvrIp\":\"192.168.1.100\"," - + "\"sdkPort\":8000," - + "\"rtspPort\":554," - + "\"channel\":1," - + "\"streamType\":1," - + "\"username\":\"admin\"," - + "\"password\":\"123456\"," - + "\"startTime\":\"2026-07-13 12:00:00\"," - + "\"endTime\":\"2026-07-13 13:00:00\"" - + "}"; - - @Mock - private HikNvrPlaybackService playbackService; - @Mock - private HikNvrPlaybackClipService playbackClipService; - - private MockMvc mockMvc; - private HikNvrPlaybackController controller; - - /** - * 创建控制器并注入录像查询及片段下载服务替身。 - */ - @Before - public void setUp() { - controller = new HikNvrPlaybackController(); - ReflectionTestUtils.setField(controller, "playbackService", playbackService); - ReflectionTestUtils.setField( - controller, "playbackClipService", playbackClipService); - mockMvc = standaloneSetup(controller) - .setControllerAdvice(new HikNvrPlaybackExceptionHandler()) - .build(); - } - - @Test - public void returnsActualTimeRangeAndPlaybackUrl() throws Exception { - String expectedUrl = "rtsp://admin:123456@192.168.1.100:554/Streaming/tracks/101" - + "?starttime=20260713T043000Z&endtime=20260713T050000Z"; - HikNvrPlaybackRecord record = HikNvrPlaybackRecord.builder() - .timeRange(Arrays.asList("20260713123000", "20260713130000")) - .rtspUrl(expectedUrl) - .build(); - when(playbackService.search(any())) - .thenReturn(HikNvrPlaybackResponse.success(Collections.singletonList(record))); - - mockMvc.perform(post("/hik/nvr/playback/search") - .contentType(MediaType.APPLICATION_JSON) - .content(REQUEST_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.code").value(0)) - .andExpect(jsonPath("$.hasRecord").value(true)) - .andExpect(jsonPath("$.records[0].timeRange[0]").value("20260713123000")) - .andExpect(jsonPath("$.records[0].timeRange[1]").value("20260713130000")) - .andExpect(jsonPath("$.records[0].rtspUrl").value(expectedUrl)); - } - - @Test - public void mapsInvalidRequestToHttp400() throws Exception { - when(playbackService.search(any())).thenThrow( - HikNvrPlaybackException.badRequest("结束时间必须晚于开始时间")); - - mockMvc.perform(post("/hik/nvr/playback/search") - .contentType(MediaType.APPLICATION_JSON) - .content(REQUEST_JSON)) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.code").value(-1)) - .andExpect(jsonPath("$.message").value("结束时间必须晚于开始时间")); - } - - @Test - public void mapsSdkFailureToHttp502WithSdkCode() throws Exception { - when(playbackService.search(any())).thenThrow( - HikNvrPlaybackException.sdkFailure(7, "连接海康NVR失败,SDK错误码:7")); - - mockMvc.perform(post("/hik/nvr/playback/search") - .contentType(MediaType.APPLICATION_JSON) - .content(REQUEST_JSON)) - .andExpect(status().isBadGateway()) - .andExpect(jsonPath("$.code").value(7)) - .andExpect(jsonPath("$.message").value("连接海康NVR失败,SDK错误码:7")); - } - - @Test - public void mapsUnreadableJsonToDocumentedHttp400Body() throws Exception { - mockMvc.perform(post("/hik/nvr/playback/search") - .contentType(MediaType.APPLICATION_JSON) - .content("{\"sdkPort\":\"not-a-number\"}")) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.code").value(-1)) - .andExpect(jsonPath("$.message") - .value("请求体格式错误,请检查JSON字段类型和格式")); - } - - /** - * 验证片段下载响应包含MP4类型、附件文件名、长度并流式写出文件。 - */ - @Test - public void returnsPreparedClipAsMp4Attachment() throws Exception { - // 下载接口只接收设备树返回的完整 cameraCode,不接收页面提交的 NVR IP 和通道。 - HikNvrPlaybackClipRequest request = HikNvrPlaybackClipRequest.builder() - .cameraCode("22078710632508667002#domain-a") - .centerTime("2026-07-28 11:55:00") - .build(); - HikNvrPlaybackClipFile clipFile = new HikNvrPlaybackClipFile( - Paths.get("clip.mp4"), - "hik-22078710632508667002-domain-a-20260728-115430_20260728-115530.mp4", - 9L, - LocalDateTime.of(2026, 7, 28, 11, 54, 30), - LocalDateTime.of(2026, 7, 28, 11, 55, 30)); - when(playbackClipService.createClip(request)).thenReturn(clipFile); - // 用固定字节模拟服务把临时MP4写入HTTP响应。 - doAnswer(invocation -> { - ByteArrayOutputStream output = - (ByteArrayOutputStream) invocation.getArgument(1); - output.write("final-mp4".getBytes(StandardCharsets.UTF_8)); - return null; - }).when(playbackClipService).writeAndDelete( - eq(clipFile), any(ByteArrayOutputStream.class)); - - // 直接调用控制器并执行流式响应体,验证响应头和写入委托。 - ResponseEntity response = - controller.downloadClip(request); - ByteArrayOutputStream output = new ByteArrayOutputStream(); - response.getBody().writeTo(output); - - assertEquals("video/mp4", - response.getHeaders().getContentType().toString()); - assertEquals(9L, response.getHeaders().getContentLength()); - assertTrue(response.getHeaders().getFirst("Content-Disposition") - .contains(clipFile.getDownloadFileName())); - assertArrayEquals( - "final-mp4".getBytes(StandardCharsets.UTF_8), - output.toByteArray()); - verify(playbackClipService).writeAndDelete(clipFile, output); - } -} diff --git a/src/test/java/com/inspect/nvr/hik/controller/HikNvrPlaybackHlsControllerTest.java b/src/test/java/com/inspect/nvr/hik/controller/HikNvrPlaybackHlsControllerTest.java deleted file mode 100644 index f52f7f4b..00000000 --- a/src/test/java/com/inspect/nvr/hik/controller/HikNvrPlaybackHlsControllerTest.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.inspect.nvr.hik.controller; - -import com.inspect.nvr.hik.domain.HikNvrPlaybackHlsSession; -import com.inspect.nvr.hik.domain.HikNvrPlaybackHlsStatus; -import com.inspect.nvr.hik.exception.HikNvrPlaybackException; -import com.inspect.nvr.hik.service.HikNvrPlaybackHlsService; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.util.ReflectionTestUtils; - -import java.util.Arrays; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; - -@RunWith(MockitoJUnitRunner.class) -public class HikNvrPlaybackHlsControllerTest { - - private static final String REQUEST_JSON = "{" - + "\"nvrIp\":\"192.168.1.100\"," - + "\"sdkPort\":8000," - + "\"rtspPort\":554," - + "\"channel\":33," - + "\"streamType\":1," - + "\"username\":\"admin\"," - + "\"password\":\"123456\"," - + "\"startTime\":\"2026-07-13 12:00:00\"," - + "\"endTime\":\"2026-07-13 13:00:00\"" - + "}"; - - @Mock - private HikNvrPlaybackHlsService playbackHlsService; - - private MockMvc mockMvc; - - @Before - public void setUp() { - HikNvrPlaybackHlsController controller = new HikNvrPlaybackHlsController(); - ReflectionTestUtils.setField(controller, "playbackHlsService", playbackHlsService); - mockMvc = standaloneSetup(controller) - .setControllerAdvice(new HikNvrPlaybackExceptionHandler()) - .build(); - } - - @Test - public void createsHlsSessionWithoutExposingRtspCredentials() throws Exception { - when(playbackHlsService.create(any())).thenReturn(session(1, 1, 1)); - - mockMvc.perform(post("/hik/nvr/playback/hls/sessions") - .contentType(MediaType.APPLICATION_JSON) - .content(REQUEST_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.hlsUrl").value( - "http://127.0.0.1:18081/playback/a1-1/hls.m3u8")) - .andExpect(jsonPath("$.rtspUrl").doesNotExist()) - .andExpect(jsonPath("$.speed").value(1)); - } - - @Test - public void sendsSeekAndNativeSpeedControlToExistingSession() throws Exception { - when(playbackHlsService.control(eq("a1"), any())).thenReturn(session(2, 8, 2)); - - mockMvc.perform(post("/hik/nvr/playback/hls/sessions/a1/controls") - .contentType(MediaType.APPLICATION_JSON) - .content("{\"action\":\"SET_SPEED\",\"speed\":8}")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.speed").value(8)); - - mockMvc.perform(post("/hik/nvr/playback/hls/sessions/a1/controls") - .contentType(MediaType.APPLICATION_JSON) - .content("{\"action\":\"SEEK\",\"seekTime\":\"2026-07-13 12:45:00\"}")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.generation").value(2)) - .andExpect(jsonPath("$.hlsUrl").value( - "http://127.0.0.1:18081/playback/a1-2/hls.m3u8")); - } - - @Test - public void exposesSessionLookupDeleteAndDocumentedErrors() throws Exception { - when(playbackHlsService.get("a1")).thenReturn(session(1, 1, 1)); - when(playbackHlsService.get("missing")) - .thenThrow(HikNvrPlaybackException.notFound("回放会话不存在或已停止")); - - mockMvc.perform(get("/hik/nvr/playback/hls/sessions/a1")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.sessionId").value("a1")); - mockMvc.perform(delete("/hik/nvr/playback/hls/sessions/a1")) - .andExpect(status().isNoContent()); - mockMvc.perform(get("/hik/nvr/playback/hls/sessions/missing")) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.code").value(-4)); - } - - private HikNvrPlaybackHlsSession session(int generation, int speed, int urlGeneration) { - return HikNvrPlaybackHlsSession.builder() - .sessionId("a1") - .status(HikNvrPlaybackHlsStatus.PLAYING) - .hlsUrl("http://127.0.0.1:18081/playback/a1-" + urlGeneration + "/hls.m3u8") - .generation(generation) - .speed(speed) - .actualTimeRange(Arrays.asList("20260713123000", "20260713130000")) - .currentTime("20260713124500") - .build(); - } -} diff --git a/src/test/java/com/inspect/nvr/hik/controller/HikNvrPlaybackUiControllerTest.java b/src/test/java/com/inspect/nvr/hik/controller/HikNvrPlaybackUiControllerTest.java deleted file mode 100644 index abea3b7d..00000000 --- a/src/test/java/com/inspect/nvr/hik/controller/HikNvrPlaybackUiControllerTest.java +++ /dev/null @@ -1,254 +0,0 @@ -package com.inspect.nvr.hik.controller; - -import com.inspect.nvr.domain.Infrared.NvrInfo; -import com.inspect.nvr.hik.domain.HikNvrCameraTarget; -import com.inspect.nvr.hik.domain.HikNvrPlaybackHlsControlAction; -import com.inspect.nvr.hik.domain.HikNvrPlaybackHlsControlRequest; -import com.inspect.nvr.hik.domain.HikNvrPlaybackHlsSession; -import com.inspect.nvr.hik.domain.HikNvrPlaybackHlsStatus; -import com.inspect.nvr.hik.domain.HikNvrPlaybackRequest; -import com.inspect.nvr.hik.domain.HikNvrPlaybackUiConfig; -import com.inspect.nvr.hik.exception.HikNvrPlaybackException; -import com.inspect.nvr.hik.service.HikNvrCameraTargetService; -import com.inspect.nvr.hik.service.HikNvrDeviceTreeService; -import com.inspect.nvr.hik.service.HikNvrPlaybackHlsService; -import com.inspect.nvr.hik.service.HikNvrPlaybackRequestFactory; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.http.MediaType; -import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.test.web.servlet.MockMvc; - -import java.util.Arrays; -import java.util.Collections; - -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; - -/** - * 验证回放控制台严格使用数据库设备树和数据库 NVR 连接参数。 - */ -@RunWith(MockitoJUnitRunner.class) -public class HikNvrPlaybackUiControllerTest { - - /** 测试设备树与页面请求共同使用的 IVS 完整摄像机编码。 */ - private static final String CAMERA_CODE = - "22078710632508667003#domain-a"; - - /** 模拟浏览器 HLS 会话创建服务。 */ - @Mock - private HikNvrPlaybackHlsService playbackHlsService; - /** 模拟数据库设备树服务。 */ - @Mock - private HikNvrDeviceTreeService deviceTreeService; - /** 模拟 cameraCode 到数据库 NVR 与通道目标的解析服务。 */ - @Mock - private HikNvrCameraTargetService cameraTargetService; - - /** 独立控制器测试使用的 MockMvc 客户端。 */ - private MockMvc mockMvc; - - /** 初始化控制器数据库依赖,避免测试访问真实设备或数据库。 */ - @Before - public void setUp() { - // 控制器只注入数据库服务和纯请求工厂,不再创建固定 NVR 配置对象。 - HikNvrPlaybackUiController controller = new HikNvrPlaybackUiController(); - ReflectionTestUtils.setField(controller, "playbackHlsService", playbackHlsService); - ReflectionTestUtils.setField(controller, "deviceTreeService", deviceTreeService); - ReflectionTestUtils.setField( - controller, "cameraTargetService", cameraTargetService); - ReflectionTestUtils.setField( - controller, "playbackRequestFactory", - new HikNvrPlaybackRequestFactory()); - // 注册统一异常处理器,保持响应格式与生产环境一致。 - mockMvc = standaloneSetup(controller) - .setControllerAdvice(new HikNvrPlaybackExceptionHandler()) - .build(); - } - - /** - * 配置接口必须原样返回数据库设备树,同时禁止暴露 NVR 密码。 - * - * @throws Exception MockMvc 请求或断言失败时抛出 - */ - @Test - public void configDoesNotExposePassword() throws Exception { - // 构造一台数据库 NVR 及其 ivs_camera 子节点。 - HikNvrPlaybackUiConfig.Channel channel = - HikNvrPlaybackUiConfig.Channel.builder() - .channel(3) - .cameraCode(CAMERA_CODE) - .nvrId(2L) - .nvrIp("192.168.1.250") - .nvrName("二号 NVR") - .name("数据库摄像机") - .group("东门") - .build(); - HikNvrPlaybackUiConfig databaseConfig = - HikNvrPlaybackUiConfig.builder() - .deviceName("二号 NVR") - .timezone("Asia/Shanghai") - .configured(true) - .nvrIp("192.168.1.250") - .defaultNvrIp("192.168.1.250") - .defaultChannel(3) - .defaultCameraCode(CAMERA_CODE) - .channels(Collections.singletonList(channel)) - .nvrs(Collections.singletonList( - HikNvrPlaybackUiConfig.Nvr.builder() - .nvrId(2L) - .name("二号 NVR") - .nvrIp("192.168.1.250") - .configured(true) - .channels(Collections.singletonList(channel)) - .build())) - .build(); - when(deviceTreeService.loadConfig()).thenReturn(databaseConfig); - - // 请求页面配置并校验 NVR 层级、摄像机归属和敏感字段边界。 - mockMvc.perform(get("/hik/nvr/playback/hls/ui/config")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.deviceName").value("二号 NVR")) - .andExpect(jsonPath("$.defaultNvrIp").value("192.168.1.250")) - .andExpect(jsonPath("$.defaultCameraCode").value(CAMERA_CODE)) - .andExpect(jsonPath("$.nvrs[0].nvrId").value(2)) - .andExpect(jsonPath("$.nvrs[0].nvrIp").value("192.168.1.250")) - .andExpect(jsonPath("$.channels[0].channel").value(3)) - .andExpect(jsonPath("$.channels[0].cameraCode").value(CAMERA_CODE)) - .andExpect(jsonPath("$.channels[0].nvrIp").value("192.168.1.250")) - .andExpect(jsonPath("$.channels[0].name").value("数据库摄像机")) - .andExpect(jsonPath("$.password").doesNotExist()); - verify(deviceTreeService).loadConfig(); - } - - /** - * 页面选择 cameraCode 后,内部回放请求必须使用其数据库 NVR 参数与通道。 - * - * @throws Exception MockMvc 请求或断言失败时抛出 - */ - @Test - public void pageRequestUsesSelectedDatabaseNvr() throws Exception { - // 模拟从 cameraCode 精确解析数据库 NVR、通道和服务端凭据。 - when(cameraTargetService.resolve(CAMERA_CODE)).thenReturn( - new HikNvrCameraTarget(CAMERA_CODE, 3, databaseNvrInfo())); - when(playbackHlsService.create(any(HikNvrPlaybackRequest.class))) - .thenReturn(session()); - - // 页面只提交设备树中的 cameraCode、码流和时间范围。 - mockMvc.perform(post("/hik/nvr/playback/hls/ui/sessions") - .contentType(MediaType.APPLICATION_JSON) - .content("{\"cameraCode\":\"" + CAMERA_CODE + "\"," - + "\"streamType\":1," - + "\"startTime\":\"2026-07-13 12:00:00\"," - + "\"endTime\":\"2026-07-13 13:00:00\"}")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.sessionId").value("ui-session")); - - // 捕获内部请求,确认所有设备连接字段都来自数据库服务。 - ArgumentCaptor captor = - ArgumentCaptor.forClass(HikNvrPlaybackRequest.class); - verify(cameraTargetService).resolve(CAMERA_CODE); - verify(playbackHlsService).create(captor.capture()); - HikNvrPlaybackRequest request = captor.getValue(); - assertEquals(CAMERA_CODE, request.getCameraCode()); - assertEquals("192.168.1.250", request.getNvrIp()); - assertEquals(Integer.valueOf(9000), request.getSdkPort()); - assertEquals(Integer.valueOf(8554), request.getRtspPort()); - assertEquals(Integer.valueOf(3), request.getChannel()); - assertEquals("database-admin", request.getUsername()); - assertEquals("database-secret", request.getPassword()); - } - - /** - * 未提交 cameraCode 时必须拒绝请求,不得选择第一台或固定设备。 - * - * @throws Exception MockMvc 请求或断言失败时抛出 - */ - @Test - public void pageRequestFailsWhenCameraCodeIsMissing() throws Exception { - // 解析服务负责统一返回缺少 cameraCode 的参数错误。 - when(cameraTargetService.resolve(null)).thenThrow( - HikNvrPlaybackException.badRequest("cameraCode不能为空")); - mockMvc.perform(post("/hik/nvr/playback/hls/ui/sessions") - .contentType(MediaType.APPLICATION_JSON) - .content("{\"streamType\":1," - + "\"startTime\":\"2026-07-13 12:00:00\"," - + "\"endTime\":\"2026-07-13 13:00:00\"}")) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.code").value(-1)) - .andExpect(jsonPath("$.message").value("cameraCode不能为空")); - verify(cameraTargetService).resolve(null); - } - - /** - * 页面暂停和恢复必须把 cameraCode 交给会话服务做绑定校验。 - * - * @throws Exception MockMvc 请求或断言失败时抛出 - */ - @Test - public void controlDelegatesCameraCodeAndAction() throws Exception { - // 控制服务返回固定会话,便于验证控制器只负责传递 cameraCode 和动作。 - when(playbackHlsService.controlByCameraCode( - any(String.class), any(HikNvrPlaybackHlsControlRequest.class))) - .thenReturn(session()); - - mockMvc.perform(post( - "/hik/nvr/playback/hls/ui/sessions/ui-session/controls") - .contentType(MediaType.APPLICATION_JSON) - .content("{\"cameraCode\":\"" + CAMERA_CODE - + "\",\"action\":\"PAUSE\"}")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.sessionId").value("ui-session")); - - // 捕获控制请求,确认前端设备标识没有退回 NVR IP 或通道号。 - ArgumentCaptor captor = - ArgumentCaptor.forClass(HikNvrPlaybackHlsControlRequest.class); - verify(playbackHlsService).controlByCameraCode( - org.mockito.ArgumentMatchers.eq("ui-session"), captor.capture()); - assertEquals(CAMERA_CODE, captor.getValue().getCameraCode()); - assertEquals(HikNvrPlaybackHlsControlAction.PAUSE, - captor.getValue().getAction()); - } - - /** 创建模拟数据库返回的完整 NVR 连接信息。 */ - private NvrInfo databaseNvrInfo() { - // 端口使用非默认值,证明请求工厂没有使用代码中的 8000/554 常量。 - NvrInfo nvrInfo = new NvrInfo(); - nvrInfo.setNvrName("二号 NVR"); - nvrInfo.setNvrIp("192.168.1.250"); - nvrInfo.setServerPort(9000); - nvrInfo.setRtspPort(8554); - nvrInfo.setHttpPort(8080); - nvrInfo.setAccount("database-admin"); - nvrInfo.setPassword("database-secret"); - nvrInfo.setStartChannel(3); - return nvrInfo; - } - - /** 构造 HLS 服务成功返回的最小会话对象。 */ - private HikNvrPlaybackHlsSession session() { - // 返回固定会话字段,便于断言控制器没有修改服务结果。 - return HikNvrPlaybackHlsSession.builder() - .sessionId("ui-session") - .cameraCode(CAMERA_CODE) - .status(HikNvrPlaybackHlsStatus.STARTING) - .hlsUrl("http://127.0.0.1:18081/playback/ui-session-1/hls.m3u8") - .generation(1) - .speed(1) - .actualTimeRange(Arrays.asList( - "20260713120000", "20260713130000")) - .currentTime("20260713120000") - .build(); - } -} diff --git a/src/test/java/com/inspect/nvr/hik/controller/HikNvrUiActionControllerTest.java b/src/test/java/com/inspect/nvr/hik/controller/HikNvrUiActionControllerTest.java deleted file mode 100644 index 29ffe950..00000000 --- a/src/test/java/com/inspect/nvr/hik/controller/HikNvrUiActionControllerTest.java +++ /dev/null @@ -1,152 +0,0 @@ -package com.inspect.nvr.hik.controller; - -import com.inspect.nvr.dahua.DahuaIvsVendorService; -import com.inspect.nvr.domain.Infrared.NvrInfo; -import com.inspect.nvr.hik.domain.HikNvrLiveStreamResponse; -import com.inspect.nvr.hik.domain.HikNvrUiActionResponse; -import com.inspect.nvr.hik.service.HikNvrCredentialService; -import com.inspect.nvr.hik.service.HikNvrLiveStreamService; -import com.inspect.nvr.hik.service.HikNvrUiActionService; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.util.ReflectionTestUtils; - -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; - -@RunWith(MockitoJUnitRunner.class) -public class HikNvrUiActionControllerTest { - - /** 实时预览测试使用的 IVS 完整摄像机编码。 */ - private static final String CAMERA_CODE = - "22078710632508667002#domain-a"; - - @Mock - private HikNvrUiActionService actionService; - - @Mock - private HikNvrLiveStreamService liveStreamService; - - @Mock - private HikNvrCredentialService credentialService; - - /** 大华预置位服务模拟对象。 */ - @Mock - private DahuaIvsVendorService dahuaVendorService; - - private MockMvc mockMvc; - - @Before - public void setUp() { - HikNvrUiActionController controller = new HikNvrUiActionController(); - ReflectionTestUtils.setField(controller, "actionService", actionService); - ReflectionTestUtils.setField(controller, "liveStreamService", liveStreamService); - ReflectionTestUtils.setField(controller, "credentialService", credentialService); - ReflectionTestUtils.setField(controller, "dahuaVendorService", dahuaVendorService); - mockMvc = standaloneSetup(controller) - .setControllerAdvice(new HikNvrPlaybackExceptionHandler()) - .build(); - } - - /** 数据库厂商为大华时,现有工作台保存接口必须切换到 NetSDK 实现。 */ - @Test - public void createPresetRoutesDahuaNvrByDatabaseVendor() throws Exception { - NvrInfo nvrInfo = new NvrInfo(); - nvrInfo.setNvrCompany("DAHUA"); - nvrInfo.setNvrIp("192.168.1.108"); - when(credentialService.resolveByIp("192.168.1.108")).thenReturn(nvrInfo); - when(dahuaVendorService.createPreset(any())).thenReturn( - HikNvrUiActionResponse.builder() - .success(true) - .message("已保存大华预置位 6") - .build()); - - mockMvc.perform(post("/hik/nvr/playback/hls/ui/actions/preset/create") - .contentType(MediaType.APPLICATION_JSON) - .content("{\"cameraCode\":\"" + CAMERA_CODE + "\"," - + "\"nvrIp\":\"192.168.1.108\"," - + "\"channel\":2,\"preset\":6,\"presetName\":\"大门\"}")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.message").value("已保存大华预置位 6")); - - // 大华目标不能再进入海康预置位服务。 - verify(dahuaVendorService).createPreset(any()); - verify(actionService, never()).createPreset(any()); - } - - @Test - public void createPresetDelegatesChannelNumberAndName() throws Exception { - // 页面所选 NVR 必须先由数据库凭据服务解析,再创建绑定该设备的操作服务。 - NvrInfo nvrInfo = new NvrInfo(); - nvrInfo.setNvrIp("192.168.1.250"); - nvrInfo.setServerPort(8000); - nvrInfo.setRtspPort(554); - nvrInfo.setHttpPort(80); - nvrInfo.setAccount("admin"); - nvrInfo.setPassword("secret"); - when(credentialService.resolveByIp("192.168.1.250")).thenReturn(nvrInfo); - when(actionService.forNvr(nvrInfo)).thenReturn(actionService); - when(actionService.createPreset(any())).thenReturn(HikNvrUiActionResponse.builder() - .success(true) - .message("已保存预置位 5(ISAPI),名称:大门") - .build()); - - mockMvc.perform(post("/hik/nvr/playback/hls/ui/actions/preset/create") - .contentType(MediaType.APPLICATION_JSON) - .content("{\"nvrIp\":\"192.168.1.250\"," - + "\"channel\":2,\"preset\":5,\"presetName\":\"大门\"}")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.message").value("已保存预置位 5(ISAPI),名称:大门")); - - ArgumentCaptor captor = - ArgumentCaptor.forClass(com.inspect.nvr.hik.domain.HikNvrUiActionRequest.class); - verify(actionService).createPreset(captor.capture()); - assertEquals("192.168.1.250", captor.getValue().getNvrIp()); - assertEquals(Integer.valueOf(2), captor.getValue().getChannel()); - assertEquals(Integer.valueOf(5), captor.getValue().getPreset()); - assertEquals("大门", captor.getValue().getPresetName()); - } - - /** - * 实时预览入口必须只把 cameraCode 和码流类型交给 IVS 实时拉流服务。 - * - * @throws Exception MockMvc 请求或断言失败时抛出 - */ - @Test - public void liveStartDelegatesCameraCodeAndStreamType() throws Exception { - // 模拟实时服务返回已由 IVS RTSP 地址启动的浏览器播放会话。 - when(liveStreamService.start(CAMERA_CODE, 1)).thenReturn( - HikNvrLiveStreamResponse.builder() - .cameraCode(CAMERA_CODE) - .streamType(1) - .status("PLAYING") - .hlsUrl("http://127.0.0.1/live/camera/hls.m3u8") - .build()); - - mockMvc.perform(post("/hik/nvr/playback/hls/ui/actions/live/start") - .contentType(MediaType.APPLICATION_JSON) - .content("{\"cameraCode\":\"" + CAMERA_CODE - + "\",\"streamType\":1}")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.cameraCode").value(CAMERA_CODE)) - .andExpect(jsonPath("$.status").value("PLAYING")); - - // 控制器不得再解析或传递页面提交的 NVR IP 与通道号。 - verify(liveStreamService).start(CAMERA_CODE, 1); - } -} diff --git a/src/test/java/com/inspect/nvr/hik/domain/HikNvrPlaybackHlsControlRequestTest.java b/src/test/java/com/inspect/nvr/hik/domain/HikNvrPlaybackHlsControlRequestTest.java deleted file mode 100644 index 54d0105e..00000000 --- a/src/test/java/com/inspect/nvr/hik/domain/HikNvrPlaybackHlsControlRequestTest.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.inspect.nvr.hik.domain; - -import org.junit.Test; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class HikNvrPlaybackHlsControlRequestTest { - - @Test - public void acceptsOnlyNativeNvrPlaybackSpeeds() { - assertTrue(HikNvrPlaybackHlsControlRequest.isSupportedSpeed(1)); - assertTrue(HikNvrPlaybackHlsControlRequest.isSupportedSpeed(2)); - assertTrue(HikNvrPlaybackHlsControlRequest.isSupportedSpeed(4)); - assertTrue(HikNvrPlaybackHlsControlRequest.isSupportedSpeed(8)); - assertFalse(HikNvrPlaybackHlsControlRequest.isSupportedSpeed(3)); - assertFalse(HikNvrPlaybackHlsControlRequest.isSupportedSpeed(null)); - } -} diff --git a/src/test/java/com/inspect/nvr/hik/service/HikFfmpegH264PublisherTest.java b/src/test/java/com/inspect/nvr/hik/service/HikFfmpegH264PublisherTest.java deleted file mode 100644 index f9c49133..00000000 --- a/src/test/java/com/inspect/nvr/hik/service/HikFfmpegH264PublisherTest.java +++ /dev/null @@ -1,253 +0,0 @@ -package com.inspect.nvr.hik.service; - -import org.junit.Test; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class HikFfmpegH264PublisherTest { - - @Test - public void usesPipeInputAndPublishesToSessionRtmpUrl() { - CapturingProcessFactory processFactory = new CapturingProcessFactory(new TestProcess()); - HikFfmpegH264Publisher publisher = new HikFfmpegH264Publisher( - processFactory, - "C:/tools/ffmpeg/bin/ffmpeg.exe", - "rtmp://127.0.0.1:1935", - 4); - - publisher.start("a1-1"); - - assertTrue(processFactory.command().contains("pipe:0")); - assertTrue(processFactory.command().contains("rtmp://127.0.0.1:1935/playback/a1-1")); - publisher.close(); - } - - @Test - public void doesNotUseArrivalWallClockForRawH264Timestamps() { - CapturingProcessFactory processFactory = new CapturingProcessFactory(new TestProcess()); - HikFfmpegH264Publisher publisher = new HikFfmpegH264Publisher( - processFactory, - "ffmpeg", - "rtmp://127.0.0.1:1935", - 4); - - publisher.start("a1-1"); - - assertFalse(processFactory.command().contains("-use_wallclock_as_timestamps")); - publisher.close(); - } - - @Test - public void forwardsRawH264WithoutRequiringDecoderSupport() { - CapturingProcessFactory processFactory = new CapturingProcessFactory(new TestProcess()); - HikFfmpegH264Publisher publisher = new HikFfmpegH264Publisher( - processFactory, - "ffmpeg", - "rtmp://127.0.0.1:1935", - 4); - - publisher.start("a1-1"); - - // 原码流转发兼容海康部分通道使用的H264数据分区帧。 - assertTrue(processFactory.command().contains("copy")); - assertTrue(processFactory.command().contains("-framerate")); - assertTrue(processFactory.command().contains("25")); - assertFalse(processFactory.command().contains("libx264")); - publisher.close(); - } - - @Test - public void scalesRawH264FrameRateForNativeFastPlayback() { - CapturingProcessFactory processFactory = new CapturingProcessFactory(new TestProcess()); - HikFfmpegH264Publisher publisher = new HikFfmpegH264Publisher( - processFactory, - "ffmpeg", - "rtmp://127.0.0.1:1935", - 4); - - publisher.start("a1-4", 4); - - int frameRateOption = processFactory.command().indexOf("-framerate"); - // 四倍速NVR帧必须按100fps生成时间戳,避免HLS窗口被浏览器快速耗尽。 - assertEquals("100", processFactory.command().get(frameRateOption + 1)); - int timestampFilterOption = processFactory.command().indexOf("-bsf:v"); - // 原始H264没有PTS/DTS,流复制时必须显式给每个包补100fps连续时间戳。 - assertEquals( - "setts=pts=N/(100*TB):dts=N/(100*TB):duration=1/(100*TB)", - processFactory.command().get(timestampFilterOption + 1)); - publisher.close(); - } - - /** - * 验证历史回放仅保留 FFmpeg 错误且关闭进度输出,避免媒体包调试内容进入 Docker 日志。 - */ - @Test - public void limitsFfmpegOutputToErrorsWithoutProgressStats() { - CapturingProcessFactory processFactory = new CapturingProcessFactory(new TestProcess()); - HikFfmpegH264Publisher publisher = new HikFfmpegH264Publisher( - processFactory, - "ffmpeg", - "rtmp://127.0.0.1:1935", - 4); - - // 启动发布器以捕获实际传给 FFmpeg 的命令行。 - publisher.start("a1-1"); - - // 日志等级必须为 error,确保 RTMP 逐包调试和十六进制转储不会写入容器日志。 - int logLevelOption = processFactory.command().indexOf("-loglevel"); - assertEquals("error", processFactory.command().get(logLevelOption + 1)); - // 长时间回放还需关闭进度行,避免无错误时仍持续输出统计信息。 - assertTrue(processFactory.command().contains("-nostats")); - assertFalse(processFactory.command().contains("debug")); - publisher.close(); - } - - @Test - public void preservesIFrameWhenQueueIsFull() throws Exception { - BlockingOutputStream output = new BlockingOutputStream(); - CapturingProcessFactory processFactory = new CapturingProcessFactory(new TestProcess(output)); - HikFfmpegH264Publisher publisher = new HikFfmpegH264Publisher( - processFactory, - "ffmpeg", - "rtmp://127.0.0.1:1935", - 2); - publisher.start("a1-1"); - - assertTrue(publisher.offer(frameOfType(2))); - assertTrue(output.awaitFirstWrite()); - assertTrue(publisher.offer(frameOfType(2))); - assertTrue(publisher.offer(frameOfType(3))); - assertTrue(publisher.offer(frameOfType(1))); - assertTrue(publisher.hasQueuedIFrame()); - - output.release(); - publisher.close(); - } - - private HikPlaybackFrame frameOfType(int packetType) { - return new HikPlaybackFrame(packetType, new byte[]{0, 0, 0, 1, 9}); - } - - private static final class CapturingProcessFactory extends HikFfmpegProcessFactory { - private final Process process; - private List command = Collections.emptyList(); - - private CapturingProcessFactory(Process process) { - this.process = process; - } - - @Override - public Process start(List command) { - this.command = new ArrayList<>(command); - return process; - } - - private List command() { - return command; - } - } - - private static final class TestProcess extends Process { - private final OutputStream output; - - private TestProcess() { - this(new ByteArrayOutputStream()); - } - - private TestProcess(OutputStream output) { - this.output = output; - } - - @Override - public OutputStream getOutputStream() { - return output; - } - - @Override - public InputStream getInputStream() { - return new ByteArrayInputStream(new byte[0]); - } - - @Override - public InputStream getErrorStream() { - return new ByteArrayInputStream(new byte[0]); - } - - @Override - public int waitFor() { - return 0; - } - - @Override - public boolean waitFor(long timeout, TimeUnit unit) { - return true; - } - - @Override - public int exitValue() { - return 0; - } - - @Override - public void destroy() { - } - - @Override - public Process destroyForcibly() { - return this; - } - } - - private static final class BlockingOutputStream extends OutputStream { - private final CountDownLatch firstWrite = new CountDownLatch(1); - private final CountDownLatch release = new CountDownLatch(1); - - @Override - public void write(int value) throws IOException { - block(); - } - - @Override - public void write(byte[] bytes, int offset, int length) throws IOException { - block(); - } - - @Override - public void close() { - release(); - } - - private void block() throws IOException { - firstWrite.countDown(); - try { - if (!release.await(5, TimeUnit.SECONDS)) { - throw new IOException("test writer timed out"); - } - } catch (InterruptedException exception) { - Thread.currentThread().interrupt(); - throw new IOException("test writer interrupted", exception); - } - } - - private boolean awaitFirstWrite() throws InterruptedException { - return firstWrite.await(2, TimeUnit.SECONDS); - } - - private void release() { - release.countDown(); - } - } -} diff --git a/src/test/java/com/inspect/nvr/hik/service/HikNvrCameraTargetServiceTest.java b/src/test/java/com/inspect/nvr/hik/service/HikNvrCameraTargetServiceTest.java deleted file mode 100644 index d700249f..00000000 --- a/src/test/java/com/inspect/nvr/hik/service/HikNvrCameraTargetServiceTest.java +++ /dev/null @@ -1,124 +0,0 @@ -package com.inspect.nvr.hik.service; - -import com.inspect.nvr.domain.Infrared.NvrInfo; -import com.inspect.nvr.hik.domain.HikNvrCameraTarget; -import com.inspect.nvr.hik.exception.HikNvrPlaybackException; -import com.inspect.nvr.ivs.mapper.IvsCameraMapper; -import com.inspect.nvr.ivs.mapper.IvsNvrMapper; -import com.inspect.nvr.ivs.pojo.IvsCamera; -import com.inspect.nvr.ivs.pojo.IvsNvr; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.http.HttpStatus; -import org.springframework.test.util.ReflectionTestUtils; - -import java.util.Collections; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.fail; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; - -/** - * 验证页面完整 cameraCode 只会解析到数据库中真实存在的摄像机、NVR 和通道。 - */ -@RunWith(MockitoJUnitRunner.class) -public class HikNvrCameraTargetServiceTest { - - /** 测试使用的完整 IVS 摄像机编码,最后三位表示通道 2。 */ - private static final String CAMERA_CODE = - "22078710632508667002#domain-a"; - /** 从测试 cameraCode 去掉末三位通道后得到的所属 NVR 编码。 */ - private static final String NVR_CODE = - "22078710632508667#domain-a"; - - /** 模拟 ivs_camera 精确编码查询。 */ - @Mock - private IvsCameraMapper ivsCameraMapper; - /** 模拟 basedata_eqpbook_nvr 设备查询。 */ - @Mock - private IvsNvrMapper ivsNvrMapper; - /** 模拟服务端数据库 NVR 凭据解析。 */ - @Mock - private HikNvrCredentialService credentialService; - - /** 待测试的 cameraCode 解析服务。 */ - private HikNvrCameraTargetService service; - - /** 创建服务并注入数据库查询替身。 */ - @Before - public void setUp() { - service = new HikNvrCameraTargetService(); - // 手工注入三个替身,确保测试不会访问真实数据库或设备。 - ReflectionTestUtils.setField(service, "ivsCameraMapper", ivsCameraMapper); - ReflectionTestUtils.setField(service, "ivsNvrMapper", ivsNvrMapper); - ReflectionTestUtils.setField( - service, "credentialService", credentialService); - } - - /** 完整 cameraCode 应精确解析所属启用 NVR、通道号和服务端凭据。 */ - @Test - public void resolvesCameraCodeToDatabaseNvrAndChannel() { - // ivs_camera 必须以完整编码精确命中真实摄像机。 - IvsCamera camera = new IvsCamera(); - camera.setCode(CAMERA_CODE); - when(ivsCameraMapper.selectByCode(CAMERA_CODE)).thenReturn(camera); - - // NVR 编码与 cameraCode 去掉末三位通道后的结果完全一致。 - IvsNvr nvr = new IvsNvr(); - nvr.setNvrCode(NVR_CODE); - nvr.setNvrIp("192.168.1.249"); - nvr.setEnabled(1); - when(ivsNvrMapper.selectAll()).thenReturn(Collections.singletonList(nvr)); - - // 完整连接参数仍由既有凭据服务从数据库读取,浏览器无法传入密码。 - NvrInfo nvrInfo = new NvrInfo(); - nvrInfo.setNvrIp("192.168.1.249"); - when(credentialService.resolveByIp("192.168.1.249")) - .thenReturn(nvrInfo); - - HikNvrCameraTarget target = service.resolve(" " + CAMERA_CODE + " "); - - assertEquals(CAMERA_CODE, target.getCameraCode()); - assertEquals(Integer.valueOf(2), target.getChannel()); - assertSame(nvrInfo, target.getNvrInfo()); - verify(ivsCameraMapper).selectByCode(CAMERA_CODE); - verify(credentialService).resolveByIp("192.168.1.249"); - } - - /** 缺少 cameraCode 时必须在访问数据库前立即返回参数错误。 */ - @Test - public void rejectsMissingCameraCodeBeforeDatabaseLookup() { - try { - // 空白输入既不能唯一定位摄像机,也不允许回退到第一台设备。 - service.resolve(" "); - fail("Expected HikNvrPlaybackException"); - } catch (HikNvrPlaybackException exception) { - assertEquals(HttpStatus.BAD_REQUEST, exception.getStatus()); - assertEquals("cameraCode不能为空", exception.getMessage()); - } - // 参数结构无效时不应触发摄像机、NVR 或凭据数据库查询。 - verifyNoInteractions( - ivsCameraMapper, ivsNvrMapper, credentialService); - } - - /** 数据库没有精确 cameraCode 时不得仅凭编码末三位推断设备。 */ - @Test - public void rejectsCameraCodeMissingFromIvsCamera() { - // 精确查询无结果,模拟页面提交已经删除或伪造的摄像机编码。 - when(ivsCameraMapper.selectByCode(CAMERA_CODE)).thenReturn(null); - try { - service.resolve(CAMERA_CODE); - fail("Expected HikNvrPlaybackException"); - } catch (HikNvrPlaybackException exception) { - assertEquals(HttpStatus.NOT_FOUND, exception.getStatus()); - } - // 摄像机不存在时不能继续扫描 NVR 或读取任何设备凭据。 - verifyNoInteractions(ivsNvrMapper, credentialService); - } -} diff --git a/src/test/java/com/inspect/nvr/hik/service/HikNvrCredentialServiceTest.java b/src/test/java/com/inspect/nvr/hik/service/HikNvrCredentialServiceTest.java deleted file mode 100644 index ddd0ee4f..00000000 --- a/src/test/java/com/inspect/nvr/hik/service/HikNvrCredentialServiceTest.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.inspect.nvr.hik.service; - -import com.inspect.nvr.domain.Infrared.NvrInfo; -import com.inspect.nvr.hik.exception.HikNvrPlaybackException; -import com.inspect.nvr.ivs.mapper.IvsNvrMapper; -import com.inspect.nvr.ivs.pojo.IvsNvr; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.test.util.ReflectionTestUtils; - -import java.util.Arrays; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; -import static org.mockito.Mockito.when; - -/** - * 验证 NVR 连接信息严格来自 basedata_eqpbook_nvr,不使用代码默认端口。 - */ -@RunWith(MockitoJUnitRunner.class) -public class HikNvrCredentialServiceTest { - - /** 模拟 basedata_eqpbook_nvr Mapper。 */ - @Mock - private IvsNvrMapper ivsNvrMapper; - - /** 待验证的数据库凭据服务。 */ - private HikNvrCredentialService service; - - /** 创建服务并注入数据库 Mapper Mock。 */ - @Before - public void setUp() { - service = new HikNvrCredentialService(); - ReflectionTestUtils.setField(service, "ivsNvrMapper", ivsNvrMapper); - } - - /** 精确 IP 查询必须原样返回数据库中的非标准端口和凭据。 */ - @Test - public void resolveByIpUsesExactDatabaseConnectionValues() { - IvsNvr databaseNvr = completeNvr( - "192.168.1.250", 9000, 8554, 8080); - when(ivsNvrMapper.selectEnabledByNvrIp("192.168.1.250")) - .thenReturn(databaseNvr); - - NvrInfo result = service.resolveByIp(" 192.168.1.250 "); - - assertEquals("192.168.1.250", result.getNvrIp()); - assertEquals(Integer.valueOf(9000), result.getServerPort()); - assertEquals(Integer.valueOf(8554), result.getRtspPort()); - assertEquals(Integer.valueOf(8080), result.getHttpPort()); - assertEquals("database-user", result.getAccount()); - assertEquals("database-password", result.getPassword()); - } - - /** 任一数据库端口为空时必须明确报错,不能自动补成 8000、554 或 80。 */ - @Test - public void resolveByIpRejectsMissingDatabasePort() { - IvsNvr incomplete = completeNvr( - "192.168.1.250", 9000, 8554, null); - when(ivsNvrMapper.selectEnabledByNvrIp("192.168.1.250")) - .thenReturn(incomplete); - - try { - service.resolveByIp("192.168.1.250"); - fail("缺少数据库 HTTP 端口时应拒绝连接"); - } catch (HikNvrPlaybackException exception) { - assertEquals("数据库中NVR连接参数未配置完整:192.168.1.250", - exception.getMessage()); - } - } - - /** 默认数据库目标应跳过禁用记录,并返回下一台连接参数完整的启用 NVR。 */ - @Test - public void resolveDefaultSelectsEnabledDatabaseRecord() { - IvsNvr disabled = completeNvr( - "192.168.1.249", 8000, 554, 80); - disabled.setEnabled(0); - IvsNvr enabled = completeNvr( - "192.168.1.250", 9000, 8554, 8080); - when(ivsNvrMapper.selectAll()) - .thenReturn(Arrays.asList(disabled, enabled)); - - NvrInfo result = service.resolveDefault(); - - assertEquals("192.168.1.250", result.getNvrIp()); - assertEquals(Integer.valueOf(9000), result.getServerPort()); - } - - /** - * 创建连接参数完整的数据库 NVR。 - * - * @param ip NVR IP - * @param sdkPort SDK 端口 - * @param rtspPort RTSP 端口 - * @param httpPort HTTP 端口 - * @return basedata_eqpbook_nvr 模拟记录 - */ - private IvsNvr completeNvr( - String ip, Integer sdkPort, Integer rtspPort, Integer httpPort) { - // 所有连接字段均显式设置,便于测试缺失任意字段时的严格校验。 - IvsNvr nvr = new IvsNvr(); - nvr.setNvrId(2L); - nvr.setPatrolDeviceName("数据库 NVR"); - nvr.setVendor("HIKVISION"); - nvr.setNvrIp(ip); - nvr.setUsername("database-user"); - nvr.setPassword("database-password"); - nvr.setSdkPort(sdkPort); - nvr.setRtspPort(rtspPort); - nvr.setHttpPort(httpPort); - nvr.setStartChannel(1); - nvr.setChannelCount(8); - nvr.setEnabled(1); - return nvr; - } -} diff --git a/src/test/java/com/inspect/nvr/hik/service/HikNvrDeviceTreeServiceTest.java b/src/test/java/com/inspect/nvr/hik/service/HikNvrDeviceTreeServiceTest.java deleted file mode 100644 index 01c8a5b0..00000000 --- a/src/test/java/com/inspect/nvr/hik/service/HikNvrDeviceTreeServiceTest.java +++ /dev/null @@ -1,184 +0,0 @@ -package com.inspect.nvr.hik.service; - -import com.inspect.nvr.hik.domain.HikNvrPlaybackUiConfig; -import com.inspect.nvr.ivs.mapper.IvsCameraMapper; -import com.inspect.nvr.ivs.mapper.IvsNvrMapper; -import com.inspect.nvr.ivs.pojo.IvsCamera; -import com.inspect.nvr.ivs.pojo.IvsNvr; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.test.util.ReflectionTestUtils; - -import java.util.Arrays; -import java.util.Collections; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; - -/** - * 验证设备树只使用 basedata_eqpbook_nvr 和 ivs_camera 构建多 NVR 层级。 - */ -@RunWith(MockitoJUnitRunner.class) -public class HikNvrDeviceTreeServiceTest { - - /** 模拟 ivs_camera 数据访问接口,测试不连接真实数据库。 */ - @Mock - private IvsCameraMapper ivsCameraMapper; - /** 模拟 basedata_eqpbook_nvr 数据访问接口。 */ - @Mock - private IvsNvrMapper ivsNvrMapper; - - /** 待验证的设备树转换服务。 */ - private HikNvrDeviceTreeService service; - - /** 创建服务并注入两个数据库 Mapper Mock。 */ - @Before - public void setUp() { - // 手动注入 Mapper,确保测试只验证数据库查询和设备树转换规则。 - service = new HikNvrDeviceTreeService(); - ReflectionTestUtils.setField(service, "ivsCameraMapper", ivsCameraMapper); - ReflectionTestUtils.setField(service, "ivsNvrMapper", ivsNvrMapper); - } - - /** 两台数据库 NVR 应分别读取自己的 ivs_camera,并提供完整 cameraCode。 */ - @Test - public void loadConfigBuildsMultipleDatabaseNvrNodes() { - // 第一台 NVR 的 start_channel=1 不在其摄像机结果中,默认应回退到第一路通道 2。 - IvsNvr firstNvr = nvr( - 1L, "一号 NVR", "192.168.1.249", 1); - // 第二台 NVR 使用相同数据库字段结构,证明设备树不再只查询固定 IP。 - IvsNvr secondNvr = nvr( - 2L, "二号 NVR", "192.168.1.250", 7); - when(ivsNvrMapper.selectAll()) - .thenReturn(Arrays.asList(firstNvr, secondNvr)); - - IvsCamera entrance = camera( - "22078710632508667002#domain-a", - "大门入口摄像机", "东门", null); - IvsCamera dome = camera( - "22078710632508668007#domain-b", - "球机", "", "球机"); - when(ivsCameraMapper.selectByNvrIp("192.168.1.249")) - .thenReturn(Collections.singletonList(entrance)); - when(ivsCameraMapper.selectByNvrIp("192.168.1.250")) - .thenReturn(Collections.singletonList(dome)); - - // 加载公开配置,所有 NVR 与摄像机字段都应来自数据库 Mock。 - HikNvrPlaybackUiConfig config = service.loadConfig(); - - // 两台启用 NVR 都必须发起各自的摄像机查询。 - verify(ivsCameraMapper).selectByNvrIp("192.168.1.249"); - verify(ivsCameraMapper).selectByNvrIp("192.168.1.250"); - assertEquals(2, config.getNvrs().size()); - assertEquals(2, config.getChannels().size()); - // 默认设备和名称来自数据库排序中的第一台有摄像机 NVR。 - assertEquals("一号 NVR", config.getDeviceName()); - assertEquals("192.168.1.249", config.getDefaultNvrIp()); - assertEquals(Integer.valueOf(2), config.getDefaultChannel()); - assertEquals("22078710632508667002#domain-a", - config.getDefaultCameraCode()); - // 扁平通道携带所属 NVR,避免两台设备的同号通道发生碰撞。 - assertEquals(Long.valueOf(1L), config.getChannels().get(0).getNvrId()); - assertEquals("192.168.1.249", config.getChannels().get(0).getNvrIp()); - assertEquals("22078710632508667002#domain-a", - config.getChannels().get(0).getCameraCode()); - assertEquals("一号 NVR", config.getChannels().get(0).getNvrName()); - assertEquals("大门入口摄像机", config.getChannels().get(0).getName()); - assertEquals("东门", config.getChannels().get(0).getGroup()); - assertEquals("192.168.1.250", config.getNvrs().get(1).getNvrIp()); - assertEquals(Integer.valueOf(7), - config.getNvrs().get(1).getChannels().get(0).getChannel()); - } - - /** NVR 表为空时必须返回真实空树,且不能扫描或伪造 ivs_camera。 */ - @Test - public void loadConfigKeepsEmptyTreeWhenNvrTableIsEmpty() { - // 数据库没有 NVR 记录时设备树没有任何安全归属条件。 - when(ivsNvrMapper.selectAll()).thenReturn(Collections.emptyList()); - - HikNvrPlaybackUiConfig config = service.loadConfig(); - - // 不允许在没有 NVR 归属时读取摄像机全表。 - verifyNoInteractions(ivsCameraMapper); - assertTrue(config.getNvrs().isEmpty()); - assertTrue(config.getChannels().isEmpty()); - assertNull(config.getNvrIp()); - assertNull(config.getDefaultChannel()); - assertNull(config.getDefaultCameraCode()); - } - - /** NVR 存在但没有摄像机时应保留真实 NVR 节点,不得生成静态通道。 */ - @Test - public void loadConfigKeepsDatabaseNvrWhenCameraTableIsEmpty() { - // 数据库 NVR 明确保存起始通道 4,设备树为空时可作为页面非摄像机默认提示值。 - IvsNvr databaseNvr = nvr( - 2L, "现场 NVR", "192.168.1.250", 4); - when(ivsNvrMapper.selectAll()) - .thenReturn(Collections.singletonList(databaseNvr)); - when(ivsCameraMapper.selectByNvrIp("192.168.1.250")) - .thenReturn(Collections.emptyList()); - - HikNvrPlaybackUiConfig config = service.loadConfig(); - - // 响应保留数据库 NVR,但摄像机列表严格保持为空。 - assertEquals(1, config.getNvrs().size()); - assertEquals("现场 NVR", config.getNvrs().get(0).getName()); - assertTrue(config.getNvrs().get(0).getChannels().isEmpty()); - assertTrue(config.getChannels().isEmpty()); - assertEquals(Integer.valueOf(4), config.getDefaultChannel()); - assertNull(config.getDefaultCameraCode()); - } - - /** - * 创建连接参数完整的数据库 NVR。 - * - * @param id NVR 主键 - * @param name 数据库设备名称 - * @param ip 数据库设备 IP - * @param startChannel 数据库起始通道 - * @return 可进入设备树并执行操作的 NVR 记录 - */ - private IvsNvr nvr(Long id, String name, String ip, Integer startChannel) { - // 所有连接字段都显式填入数据库对象,测试不依赖任何代码默认值。 - IvsNvr nvr = new IvsNvr(); - nvr.setNvrId(id); - nvr.setPatrolDeviceName(name); - nvr.setNvrIp(ip); - nvr.setNvrCode("2207871063250866" + id + "#domain-" + id); - nvr.setUsername("admin"); - nvr.setPassword("secret"); - nvr.setSdkPort(8000); - nvr.setRtspPort(554); - nvr.setHttpPort(80); - nvr.setStartChannel(startChannel); - nvr.setEnabled(1); - return nvr; - } - - /** - * 创建用于设备树转换测试的 IVS 摄像机记录。 - * - * @param code IVS 完整摄像机编码 - * @param name 数据库摄像机名称 - * @param location 数据库安装位置 - * @param model 数据库设备型号 - * @return 可由设备树服务转换的摄像机对象 - */ - private IvsCamera camera( - String code, String name, String location, String model) { - // 填充设备树实际读取的字段,其他 IVS 字段与本测试无关。 - IvsCamera camera = new IvsCamera(); - camera.setCode(code); - camera.setName(name); - camera.setCameraLocation(location); - camera.setDevModelType(model); - return camera; - } -} diff --git a/src/test/java/com/inspect/nvr/hik/service/HikNvrLiveStreamServiceTest.java b/src/test/java/com/inspect/nvr/hik/service/HikNvrLiveStreamServiceTest.java deleted file mode 100644 index 3efc7dda..00000000 --- a/src/test/java/com/inspect/nvr/hik/service/HikNvrLiveStreamServiceTest.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.inspect.nvr.hik.service; - -import com.inspect.nvr.domain.ivs.IvsRtspUrlResponse; -import com.inspect.nvr.domain.ivs.IvsVideoRtspVo; -import com.inspect.nvr.ivs.service.IvsOpenApiService; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.test.util.ReflectionTestUtils; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** - * 验证浏览器实时转码服务通过现有 IVS 门面按 cameraCode 取得 RTSP 地址。 - */ -@RunWith(MockitoJUnitRunner.class) -public class HikNvrLiveStreamServiceTest { - - /** 实时预览测试使用的 IVS 完整摄像机编码。 */ - private static final String CAMERA_CODE = - "22078710632508667002#domain-a"; - - /** 模拟现有 IVS 北向能力门面,不启动真实 NVR 或 FFmpeg。 */ - @Mock - private IvsOpenApiService ivsOpenApiService; - - /** 待验证的实时转码服务。 */ - private HikNvrLiveStreamService service; - - /** 创建服务并只注入既有 IVS 门面替身。 */ - @Before - public void setUp() { - service = new HikNvrLiveStreamService(); - // 本测试只验证取得 RTSP 的边界,不进入 FFmpeg 和媒体网关启动逻辑。 - ReflectionTestUtils.setField( - service, "ivsOpenApiService", ivsOpenApiService); - ReflectionTestUtils.setField(service, "ffmpegPath", "ffmpeg"); - } - - /** cameraCode 和页面码流类型必须原样进入现有 IVS RTSP 请求对象。 */ - @Test - public void resolvesRealtimeRtspThroughExistingIvsFacade() { - // IVS 门面返回固定 RTSP 地址,避免测试访问真实设备。 - when(ivsOpenApiService.rtspUrl(any(IvsVideoRtspVo.class))) - .thenReturn(IvsRtspUrlResponse.builder() - .resultCode(0) - .rtspURL("rtsp://192.168.1.249/live") - .build()); - - String rtspUrl = ReflectionTestUtils.invokeMethod( - service, "resolveLiveRtspUrl", CAMERA_CODE, 2); - - assertEquals("rtsp://192.168.1.249/live", rtspUrl); - // 捕获请求,确认没有回退到 NVR IP 和页面通道参数。 - ArgumentCaptor captor = - ArgumentCaptor.forClass(IvsVideoRtspVo.class); - verify(ivsOpenApiService).rtspUrl(captor.capture()); - assertEquals(CAMERA_CODE, captor.getValue().getCameraCode()); - assertEquals(1, captor.getValue().getMediaURLParam().getServiceType()); - assertEquals(2, captor.getValue().getMediaURLParam().getStreamType()); - } - - /** 大华 realmonitor 的 HEVC 实时流必须转成最高 1080p 的兼容 H.264。 */ - @Test - public void transcodesDahuaRealtimeStreamToH264() { - java.util.List command = ReflectionTestUtils.invokeMethod( - service, - "buildFfmpegCommand", - "rtsp://192.168.1.134/cam/realmonitor?channel=3&subtype=0", - "rtmp://127.0.0.1:1935/live/camera-3"); - - assertTrue(command.contains("libx264")); - // 固定验证缩放、Profile 和 Level,防止后续改动重新生成旧浏览器无法解码的 2K Level 5 视频。 - assertTrue(command.contains("scale=w='min(1920,iw)':h=-2")); - assertTrue(command.contains("baseline")); - assertTrue(command.contains("4.1")); - assertTrue(command.contains("ultrafast")); - assertFalse(command.contains("copy")); - } - - /** 海康现有 H.264 实时路径继续原码流转发,避免增加多画面 CPU 占用。 */ - @Test - public void keepsHikvisionRealtimeStreamAsCopy() { - java.util.List command = ReflectionTestUtils.invokeMethod( - service, - "buildFfmpegCommand", - "rtsp://192.168.1.249/Streaming/Channels/301", - "rtmp://127.0.0.1:1935/live/camera-3"); - - assertTrue(command.contains("copy")); - assertFalse(command.contains("libx264")); - } -} diff --git a/src/test/java/com/inspect/nvr/hik/service/HikNvrPlaybackClipExporterTest.java b/src/test/java/com/inspect/nvr/hik/service/HikNvrPlaybackClipExporterTest.java deleted file mode 100644 index d154e7e1..00000000 --- a/src/test/java/com/inspect/nvr/hik/service/HikNvrPlaybackClipExporterTest.java +++ /dev/null @@ -1,175 +0,0 @@ -package com.inspect.nvr.hik.service; - -import com.inspect.nvr.domain.Infrared.NvrInfo; -import com.inspect.nvr.hik.domain.HikNvrPlaybackRequest; -import com.inspect.nvr.hik.vo.HikNvrPlaybackClipConfigVo; -import com.inspect.nvr.hikVision.utils.jna.HCNetSDK; -import com.inspect.nvr.service.HikLoginService; -import com.inspect.nvr.service.HikLoginSession; -import com.sun.jna.Pointer; -import com.sun.jna.ptr.IntByReference; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.test.util.ReflectionTestUtils; - -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.LocalDateTime; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@RunWith(MockitoJUnitRunner.class) -public class HikNvrPlaybackClipExporterTest { - - /** 模拟海康登录和通道映射。 */ - @Mock - private HikLoginService hikLoginService; - /** 模拟海康原生录像下载接口。 */ - @Mock - private HCNetSDK sdk; - /** 模拟 FFmpeg 进程创建并生成最终文件。 */ - @Mock - private HikFfmpegProcessFactory processFactory; - /** 模拟已经成功退出的 FFmpeg 子进程。 */ - @Mock - private Process process; - - /** 待测试的 SDK 下载和 MP4 封装器。 */ - private HikNvrPlaybackClipExporter exporter; - - /** - * 配置SDK下载立即完成,并让FFmpeg替身生成固定MP4字节。 - */ - @Before - public void setUp() throws Exception { - HikNvrPlaybackClipConfigVo config = new HikNvrPlaybackClipConfigVo(); - config.setFfmpegPath("ffmpeg"); - config.setDownloadTimeoutMillis(5_000L); - config.setPollIntervalMillis(10L); - config.setFfmpegTimeoutMillis(5_000L); - - exporter = new HikNvrPlaybackClipExporter(); - // 注入测试替身,避免连接真实NVR和启动本机FFmpeg。 - ReflectionTestUtils.setField(exporter, "hikLoginService", hikLoginService); - ReflectionTestUtils.setField(exporter, "sdk", sdk); - ReflectionTestUtils.setField(exporter, "ffmpegProcessFactory", processFactory); - ReflectionTestUtils.setField(exporter, "config", config); - - // 逻辑通道2映射为海康SDK实际通道34。 - when(hikLoginService.loginSession(any(NvrInfo.class))) - .thenReturn(new HikLoginSession(7, 0, 1, 8, 33)); - // SDK创建下载任务时向其目标路径写入模拟设备文件。 - when(sdk.NET_DVR_GetFileByTime_V40( - eq(7), anyString(), any(HCNetSDK.NET_DVR_PLAYCOND.class))) - .thenAnswer(invocation -> { - Path source = Paths.get((String) invocation.getArgument(1)); - Files.write(source, "nvr-source".getBytes(StandardCharsets.UTF_8)); - return 91; - }); - when(sdk.NET_DVR_PlayBackControl_V40( - eq(91), - eq(HCNetSDK.NET_DVR_PLAYSTART), - any(Pointer.class), - eq(Integer.BYTES), - isNull(), - any(IntByReference.class))) - .thenReturn(true); - when(sdk.NET_DVR_GetDownloadPos(91)).thenReturn(100); - when(sdk.NET_DVR_StopGetFile(91)).thenReturn(true); - - // FFmpeg替身把固定字节写入命令最后一个输出路径。 - when(processFactory.start(anyList())).thenAnswer(invocation -> { - List command = invocation.getArgument(0); - Path output = Paths.get(command.get(command.size() - 1)); - Files.write(output, "final-mp4".getBytes(StandardCharsets.UTF_8)); - return process; - }); - when(process.waitFor(anyLong(), eq(TimeUnit.MILLISECONDS))) - .thenReturn(true); - when(process.exitValue()).thenReturn(0); - when(process.isAlive()).thenReturn(false); - } - - /** - * 验证SDK按精确一分钟下载,并由FFmpeg生成最终MP4。 - */ - @Test - @SuppressWarnings({"rawtypes", "unchecked"}) - public void downloadsExactRangeAndRemuxesToMp4() throws Exception { - LocalDateTime start = LocalDateTime.of(2026, 7, 28, 11, 54, 30); - LocalDateTime end = LocalDateTime.of(2026, 7, 28, 11, 55, 30); - - // 执行设备下载和MP4封装。 - Path output = exporter.export(request(), start, end); - - assertArrayEquals( - "final-mp4".getBytes(StandardCharsets.UTF_8), - Files.readAllBytes(output)); - // 捕获SDK条件,确认通道映射、主码流和前后30秒时间范围。 - ArgumentCaptor conditionCaptor = - ArgumentCaptor.forClass(HCNetSDK.NET_DVR_PLAYCOND.class); - verify(sdk).NET_DVR_GetFileByTime_V40( - eq(7), anyString(), conditionCaptor.capture()); - HCNetSDK.NET_DVR_PLAYCOND condition = conditionCaptor.getValue(); - assertEquals(34, condition.dwChannel); - assertEquals(0, condition.byStreamType); - assertEquals(11, condition.struStartTime.dwHour); - assertEquals(54, condition.struStartTime.dwMinute); - assertEquals(30, condition.struStartTime.dwSecond); - assertEquals(55, condition.struStopTime.dwMinute); - assertEquals(30, condition.struStopTime.dwSecond); - verify(sdk).NET_DVR_StopGetFile(91); - - // 捕获FFmpeg命令,确认视频原码流、可选音频和faststart封装已启用。 - ArgumentCaptor> commandCaptor = ArgumentCaptor.forClass(List.class); - verify(processFactory).start(commandCaptor.capture()); - List command = commandCaptor.getValue(); - assertTrue(command.contains("0:a?")); - assertTrue(command.contains("copy")); - assertTrue(command.contains("+faststart")); - - Path directory = output.getParent(); - // 调用生产清理逻辑后,最终文件和受控独立目录都应被删除。 - exporter.cleanup(output); - assertFalse(Files.exists(output)); - assertFalse(Files.exists(directory)); - } - - /** - * 构造数据库已经补齐凭据的内部下载请求。 - * - * @return 主码流、逻辑通道2的内部请求 - */ - private HikNvrPlaybackRequest request() { - return HikNvrPlaybackRequest.builder() - .nvrIp("192.168.1.249") - .sdkPort(8000) - .rtspPort(554) - .channel(2) - .streamType(1) - .username("operator") - .password("secret") - .startTime("2026-07-28 11:54:30") - .endTime("2026-07-28 11:55:30") - .build(); - } -} diff --git a/src/test/java/com/inspect/nvr/hik/service/HikNvrPlaybackClipServiceTest.java b/src/test/java/com/inspect/nvr/hik/service/HikNvrPlaybackClipServiceTest.java deleted file mode 100644 index afc59831..00000000 --- a/src/test/java/com/inspect/nvr/hik/service/HikNvrPlaybackClipServiceTest.java +++ /dev/null @@ -1,281 +0,0 @@ -package com.inspect.nvr.hik.service; - -import com.inspect.nvr.domain.Infrared.NvrInfo; -import com.inspect.nvr.hik.domain.HikNvrCameraTarget; -import com.inspect.nvr.hik.domain.HikNvrPlaybackClipRequest; -import com.inspect.nvr.hik.domain.HikNvrPlaybackRecord; -import com.inspect.nvr.hik.domain.HikNvrPlaybackRequest; -import com.inspect.nvr.hik.domain.HikNvrPlaybackResponse; -import com.inspect.nvr.hik.domain.HikNvrPlaybackUiSessionRequest; -import com.inspect.nvr.hik.exception.HikNvrPlaybackException; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.http.HttpStatus; -import org.springframework.test.util.ReflectionTestUtils; - -import java.io.ByteArrayOutputStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.LocalDateTime; -import java.util.Arrays; -import java.util.Collections; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@RunWith(MockitoJUnitRunner.class) -public class HikNvrPlaybackClipServiceTest { - - /** 片段下载页面与服务端共同使用的完整摄像机编码。 */ - private static final String CAMERA_CODE = - "22078710632508667002#domain-a"; - - /** 模拟 cameraCode 到数据库 NVR 与通道的解析。 */ - @Mock - private HikNvrCameraTargetService cameraTargetService; - /** 模拟页面条件到内部回放请求的安全转换。 */ - @Mock - private HikNvrPlaybackRequestFactory playbackRequestFactory; - /** 模拟海康录像区间查询。 */ - @Mock - private HikNvrPlaybackService playbackService; - /** 模拟海康下载和 FFmpeg 封装。 */ - @Mock - private HikNvrPlaybackClipExporter clipExporter; - - /** 待测试的一分钟录像片段编排服务。 */ - private HikNvrPlaybackClipService service; - /** 测试使用的临时目录。 */ - private Path temporaryDirectory; - /** 测试使用的已生成 MP4 文件。 */ - private Path clipPath; - /** 数据库返回的 NVR 连接对象。 */ - private NvrInfo nvrInfo; - /** cameraCode 解析得到的内部摄像机目标。 */ - private HikNvrCameraTarget cameraTarget; - /** 统一工厂生成的内部回放请求。 */ - private HikNvrPlaybackRequest playbackRequest; - - /** - * 准备完整连续录像和一个小型临时 MP4,供各测试复用。 - */ - @Before - public void setUp() throws Exception { - service = new HikNvrPlaybackClipService(); - // 注入测试替身,隔离真实数据库、NVR SDK和FFmpeg。 - ReflectionTestUtils.setField( - service, "cameraTargetService", cameraTargetService); - ReflectionTestUtils.setField(service, "playbackRequestFactory", playbackRequestFactory); - ReflectionTestUtils.setField(service, "playbackService", playbackService); - ReflectionTestUtils.setField(service, "clipExporter", clipExporter); - - // 创建本测试独立临时文件,验证长度和流式复制逻辑。 - temporaryDirectory = Files.createTempDirectory("clip-service-test-"); - clipPath = temporaryDirectory.resolve("clip.mp4"); - Files.write(clipPath, "mp4-data".getBytes(StandardCharsets.UTF_8)); - - // 构造数据库NVR对象,服务只验证它被传给统一请求工厂。 - nvrInfo = new NvrInfo(); - nvrInfo.setNvrIp("192.168.1.249"); - cameraTarget = new HikNvrCameraTarget(CAMERA_CODE, 2, nvrInfo); - playbackRequest = HikNvrPlaybackRequest.builder() - .cameraCode(CAMERA_CODE) - .nvrIp("192.168.1.249") - .sdkPort(8000) - .rtspPort(554) - .channel(2) - .streamType(1) - .username("operator") - .password("secret") - .startTime("2026-07-28 11:54:30") - .endTime("2026-07-28 11:55:30") - .build(); - - // 默认让数据库、请求工厂、录像查询和导出链路全部成功。 - when(cameraTargetService.resolve(CAMERA_CODE)).thenReturn(cameraTarget); - when(playbackRequestFactory.create( - any(HikNvrPlaybackUiSessionRequest.class), eq(cameraTarget))) - .thenReturn(playbackRequest); - when(playbackService.search(playbackRequest)).thenReturn( - response("20260728115430", "20260728115530")); - when(clipExporter.export( - eq(playbackRequest), - eq(LocalDateTime.of(2026, 7, 28, 11, 54, 30)), - eq(LocalDateTime.of(2026, 7, 28, 11, 55, 30)))) - .thenReturn(clipPath); - } - - /** - * 删除测试自己创建的精确临时文件和目录。 - */ - @After - public void tearDown() throws Exception { - // 测试替身不会真正执行清理,因此这里删除已知文件和独立目录。 - Files.deleteIfExists(clipPath); - Files.deleteIfExists(temporaryDirectory); - } - - /** - * 验证未传秒数时生成中心时刻前后各30秒的完整一分钟。 - */ - @Test - public void defaultsToThirtySecondsBeforeAndAfterCenterTime() { - // 使用最小页面参数创建录像片段。 - HikNvrPlaybackClipFile clipFile = service.createClip(request()); - - // 捕获交给统一工厂的页面范围,确认是11:54:30到11:55:30。 - ArgumentCaptor requestCaptor = - ArgumentCaptor.forClass(HikNvrPlaybackUiSessionRequest.class); - verify(cameraTargetService).resolve(CAMERA_CODE); - verify(playbackRequestFactory).create( - requestCaptor.capture(), eq(cameraTarget)); - assertEquals(CAMERA_CODE, requestCaptor.getValue().getCameraCode()); - assertEquals("2026-07-28 11:54:30", requestCaptor.getValue().getStartTime()); - assertEquals("2026-07-28 11:55:30", requestCaptor.getValue().getEndTime()); - assertEquals(Integer.valueOf(1), requestCaptor.getValue().getStreamType()); - assertEquals(8L, clipFile.getContentLength()); - assertTrue(clipFile.getDownloadFileName().contains( - "20260728-115430_20260728-115530.mp4")); - } - - /** - * 验证目标一分钟开头缺少录像时拒绝生成不完整片段。 - */ - @Test - public void rejectsIncompleteMinuteBeforeCallingExporter() { - // 模拟设备实际录像晚10秒开始,无法覆盖前30秒。 - when(playbackService.search(playbackRequest)).thenReturn( - response("20260728115440", "20260728115530")); - try { - // 服务应在SDK下载前识别录像不完整。 - service.createClip(request()); - fail("Expected HikNvrPlaybackException"); - } catch (HikNvrPlaybackException exception) { - assertEquals(HttpStatus.CONFLICT, exception.getStatus()); - assertTrue(exception.getMessage().contains("录像不足30秒")); - } - // 录像不完整时不得创建临时设备下载任务。 - verify(clipExporter, never()).export(any(), any(), any()); - } - - /** - * 验证目标时间没有任何录像时返回可供页面弹窗展示的明确原因。 - */ - @Test - public void rejectsCenterTimeWithoutAnyRecord() { - // 模拟NVR查询成功但目标时间范围没有任何录像记录。 - when(playbackService.search(playbackRequest)).thenReturn( - HikNvrPlaybackResponse.success(Collections.emptyList())); - try { - // 服务应在创建SDK下载任务前返回404。 - service.createClip(request()); - fail("Expected HikNvrPlaybackException"); - } catch (HikNvrPlaybackException exception) { - assertEquals(HttpStatus.NOT_FOUND, exception.getStatus()); - assertTrue(exception.getMessage().contains( - "目标时间没有对应录像")); - } - // 没有录像时不能调用SDK和FFmpeg生成空文件。 - verify(clipExporter, never()).export(any(), any(), any()); - } - - /** - * 验证单侧秒数超过上限时返回参数错误。 - */ - @Test - public void rejectsContextLongerThanFiveMinutesPerSide() { - HikNvrPlaybackClipRequest request = request(); - request.setBeforeSeconds(301); - try { - // 超大下载范围必须在访问数据库和NVR前被拒绝。 - service.createClip(request); - fail("Expected HikNvrPlaybackException"); - } catch (HikNvrPlaybackException exception) { - assertEquals(HttpStatus.BAD_REQUEST, exception.getStatus()); - assertTrue(exception.getMessage().contains("beforeSeconds")); - } - // 参数校验失败时不得读取NVR连接参数。 - verify(cameraTargetService, never()).resolve(any()); - } - - /** - * 验证未来中心时刻在访问数据库和NVR前被后端拒绝。 - */ - @Test - public void rejectsCenterTimeThatHasNotArrived() { - HikNvrPlaybackClipRequest request = request(); - request.setCenterTime("2999-12-31 23:59:59"); - try { - // 未来录像尚未产生,接口应返回可供页面弹窗展示的400错误。 - service.createClip(request); - fail("Expected HikNvrPlaybackException"); - } catch (HikNvrPlaybackException exception) { - assertEquals(HttpStatus.BAD_REQUEST, exception.getStatus()); - assertTrue(exception.getMessage().contains("尚未到达")); - } - // 未来时间校验失败时不得读取NVR连接参数。 - verify(cameraTargetService, never()).resolve(any()); - } - - /** - * 验证响应流复制完成后一定调用导出器清理临时目录。 - */ - @Test - public void streamsPreparedFileAndCleansTemporaryDirectory() throws Exception { - HikNvrPlaybackClipFile clipFile = new HikNvrPlaybackClipFile( - clipPath, - "clip.mp4", - Files.size(clipPath), - LocalDateTime.of(2026, 7, 28, 11, 54, 30), - LocalDateTime.of(2026, 7, 28, 11, 55, 30)); - ByteArrayOutputStream output = new ByteArrayOutputStream(); - - // 将临时文件流式写入内存输出流,模拟HTTP客户端下载。 - service.writeAndDelete(clipFile, output); - - assertArrayEquals( - "mp4-data".getBytes(StandardCharsets.UTF_8), - output.toByteArray()); - verify(clipExporter).cleanup(clipPath); - } - - /** - * 创建默认的中心时刻下载请求。 - * - * @return 前后秒数和码流均留空的页面请求 - */ - private HikNvrPlaybackClipRequest request() { - return HikNvrPlaybackClipRequest.builder() - .cameraCode(CAMERA_CODE) - .centerTime("2026-07-28 11:55:00") - .build(); - } - - /** - * 创建一个只包含单段连续录像的查询响应。 - * - * @param start 紧凑格式开始时间 - * @param end 紧凑格式结束时间 - * @return 海康录像查询成功响应 - */ - private HikNvrPlaybackResponse response(String start, String end) { - return HikNvrPlaybackResponse.success(Collections.singletonList( - HikNvrPlaybackRecord.builder() - .timeRange(Arrays.asList(start, end)) - .rtspUrl("rtsp://redacted") - .build())); - } -} diff --git a/src/test/java/com/inspect/nvr/hik/service/HikNvrPlaybackHlsServiceTest.java b/src/test/java/com/inspect/nvr/hik/service/HikNvrPlaybackHlsServiceTest.java deleted file mode 100644 index d4a1ec9d..00000000 --- a/src/test/java/com/inspect/nvr/hik/service/HikNvrPlaybackHlsServiceTest.java +++ /dev/null @@ -1,398 +0,0 @@ -package com.inspect.nvr.hik.service; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.inspect.nvr.hik.domain.HikNvrPlaybackHlsControlAction; -import com.inspect.nvr.hik.domain.HikNvrPlaybackHlsControlRequest; -import com.inspect.nvr.hik.domain.HikNvrPlaybackHlsSession; -import com.inspect.nvr.hik.domain.HikNvrPlaybackHlsStatus; -import com.inspect.nvr.hik.domain.HikNvrPlaybackRecord; -import com.inspect.nvr.hik.domain.HikNvrPlaybackRequest; -import com.inspect.nvr.hik.domain.HikNvrPlaybackResponse; -import com.inspect.nvr.hik.exception.HikNvrPlaybackException; -import com.inspect.nvr.hik.vo.HikNvrPlaybackHlsConfigVo; -import com.inspect.nvr.hikVision.utils.jna.HCNetSDK; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.http.HttpStatus; -import org.springframework.test.util.ReflectionTestUtils; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.InputStream; -import java.io.OutputStream; -import java.time.LocalDateTime; -import java.util.Arrays; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@RunWith(MockitoJUnitRunner.class) -public class HikNvrPlaybackHlsServiceTest { - - /** UI 回放会话绑定的完整 IVS 摄像机编码。 */ - private static final String CAMERA_CODE = - "22078710632508667033#domain-a"; - - @Mock - private HikNvrPlaybackService playbackService; - @Mock - private HikSdkPlaybackGateway gateway; - @Mock - private HikSdkPlaybackHandle handle; - @Mock - private HikPlaybackHlsReadinessProbe readinessProbe; - - private HikNvrPlaybackHlsService service; - private TestProcessFactory processFactory; - - @Before - public void setUp() { - HikPlaybackHlsUrlBuilder urlBuilder = new HikPlaybackHlsUrlBuilder(); - ReflectionTestUtils.invokeMethod(urlBuilder, "initialize"); - - HikNvrPlaybackHlsConfigVo config = new HikNvrPlaybackHlsConfigVo(); - config.setFfmpegPath("ffmpeg"); - config.setRtmpBaseUrl("rtmp://127.0.0.1:1935"); - config.setQueueCapacity(4); - config.setSessionGraceMillis(60_000L); - config.setIdleSessionMillis(120_000L); - config.setStartupTimeoutMillis(30_000L); - config.setNoDataTimeoutMillis(20_000L); - config.setMaxConcurrentSessions(1); - - service = new HikNvrPlaybackHlsService(); - ReflectionTestUtils.setField(service, "playbackService", playbackService); - ReflectionTestUtils.setField(service, "sdkPlaybackGateway", gateway); - ReflectionTestUtils.setField(service, "sessionRegistry", - new HikNvrPlaybackHlsSessionRegistry()); - ReflectionTestUtils.setField(service, "hlsUrlBuilder", urlBuilder); - ReflectionTestUtils.setField(service, "readinessProbe", readinessProbe); - processFactory = new TestProcessFactory(); - ReflectionTestUtils.setField(service, "ffmpegProcessFactory", processFactory); - ReflectionTestUtils.setField(service, "config", config); - service.initialize(); - when(readinessProbe.isReady(any(String.class))).thenReturn(true); - when(playbackService.search(any())).thenReturn(HikNvrPlaybackResponse.success( - Collections.singletonList(HikNvrPlaybackRecord.builder() - .timeRange(Arrays.asList("20260713123000", "20260713130000")) - .rtspUrl("rtsp://operator:secret@192.168.1.100/Streaming/tracks/101") - .build()))); - when(gateway.open(any(), any(LocalDateTime.class), any(LocalDateTime.class), any())) - .thenReturn(handle); - } - - @Test - public void createsSessionFromOneActualRecordingRangeWithoutReturningRtspUrl() throws Exception { - HikNvrPlaybackHlsSession session = service.create(request()); - - assertEquals(HikNvrPlaybackHlsStatus.PLAYING, session.getStatus()); - assertEquals(1, session.getGeneration()); - assertEquals(Arrays.asList("20260713123000", "20260713130000"), - session.getActualTimeRange()); - assertTrue(session.getHlsUrl().contains("/playback/")); - assertTrue(session.getHlsUrl().endsWith("-1/hls.m3u8")); - assertFalse(new ObjectMapper().writeValueAsString(session).contains("rtsp://")); - } - - @Test - public void reportsStartingUntilZlmHasGeneratedAnHlsPlaylist() { - when(readinessProbe.isReady(any(String.class))).thenReturn(false); - HikNvrPlaybackHlsSession created = service.create(request()); - - assertEquals(HikNvrPlaybackHlsStatus.STARTING, created.getStatus()); - - when(readinessProbe.isReady(any(String.class))).thenReturn(true); - assertEquals(HikNvrPlaybackHlsStatus.PLAYING, - service.get(created.getSessionId()).getStatus()); - } - - @Test - public void seekCreatesNextGenerationAndReturnsNewHlsUrl() { - HikNvrPlaybackHlsSession created = service.create(request()); - - HikNvrPlaybackHlsSession moved = service.control( - created.getSessionId(), - HikNvrPlaybackHlsControlRequest.builder() - .action(HikNvrPlaybackHlsControlAction.SEEK) - .seekTime("2026-07-13 12:45:00") - .build()); - - assertEquals(HikNvrPlaybackHlsStatus.PLAYING, moved.getStatus()); - assertEquals(2, moved.getGeneration()); - assertTrue(moved.getHlsUrl().contains("-2/hls.m3u8")); - ArgumentCaptor target = ArgumentCaptor.forClass(LocalDateTime.class); - verify(gateway, times(2)).open( - any(), target.capture(), any(LocalDateTime.class), any()); - assertEquals(LocalDateTime.of(2026, 7, 13, 12, 45), target.getAllValues().get(1)); - verify(handle).close(); - } - - @Test - public void appliesNativeSpeedToSameSdkPlaybackHandle() { - HikNvrPlaybackHlsSession created = service.create(request()); - - HikNvrPlaybackHlsSession changed = service.control( - created.getSessionId(), - HikNvrPlaybackHlsControlRequest.builder() - .action(HikNvrPlaybackHlsControlAction.SET_SPEED) - .speed(8) - .build()); - - assertEquals(8, changed.getSpeed()); - assertEquals(2, changed.getGeneration()); - assertTrue(changed.getHlsUrl().endsWith("-2/hls.m3u8")); - verify(handle).setSpeed(8); - verify(handle).setFrameSink(any(HikPlaybackFrameSink.class)); - // 八倍速新代次必须按200fps生成时间戳,同时保持同一个NVR SDK连接。 - assertEquals("200", processFactory.frameRateAt(1)); - verify(gateway, times(1)).open( - any(), any(LocalDateTime.class), any(LocalDateTime.class), any()); - } - - @Test - public void reportsCurrentTimeFromLatestSdkPlaybackPacket() { - HikNvrPlaybackHlsSession created = service.create(request()); - when(handle.getLastPlaybackTime()).thenReturn(LocalDateTime.of(2026, 7, 13, 12, 45, 12)); - - HikNvrPlaybackHlsSession session = service.get(created.getSessionId()); - - assertEquals("20260713124512", session.getCurrentTime()); - } - - @Test - public void recreatesPausedPlaybackWhenPauseFailsAfterSdkConnectionEnded() { - HikNvrPlaybackHlsSession created = service.create(request()); - HikSdkPlaybackHandle replacement = org.mockito.Mockito.mock(HikSdkPlaybackHandle.class); - LocalDateTime lastPlaybackTime = LocalDateTime.of(2026, 7, 13, 12, 45, 12); - when(handle.getLastPlaybackTime()).thenReturn(lastPlaybackTime); - doThrow(HikNvrPlaybackException.sdkFailure( - HCNetSDK.NET_DVR_NETWORK_SEND_ERROR, - "连接已结束")) - .when(handle).pause(); - when(gateway.open(any(), any(LocalDateTime.class), any(LocalDateTime.class), any())) - .thenReturn(replacement); - - HikNvrPlaybackHlsSession paused = service.control( - created.getSessionId(), - HikNvrPlaybackHlsControlRequest.builder() - .action(HikNvrPlaybackHlsControlAction.PAUSE) - .build()); - - assertEquals(HikNvrPlaybackHlsStatus.PAUSED, paused.getStatus()); - assertEquals(2, paused.getGeneration()); - assertEquals("20260713124512", paused.getCurrentTime()); - assertTrue(paused.getHlsUrl().endsWith("-2/hls.m3u8")); - verify(handle).close(); - verify(replacement).pause(); - } - - @Test - public void recreatesPlayingPlaybackWhenResumeFailsAfterSdkConnectionEnded() { - HikNvrPlaybackHlsSession created = service.create(request()); - service.control(created.getSessionId(), HikNvrPlaybackHlsControlRequest.builder() - .action(HikNvrPlaybackHlsControlAction.PAUSE) - .build()); - HikSdkPlaybackHandle replacement = org.mockito.Mockito.mock(HikSdkPlaybackHandle.class); - LocalDateTime lastPlaybackTime = LocalDateTime.of(2026, 7, 13, 12, 45, 12); - when(handle.getLastPlaybackTime()).thenReturn(lastPlaybackTime); - doThrow(HikNvrPlaybackException.sdkFailure( - HCNetSDK.NET_DVR_NETWORK_SEND_ERROR, - "连接已结束")) - .when(handle).resume(); - when(gateway.open(any(), any(LocalDateTime.class), any(LocalDateTime.class), any())) - .thenReturn(replacement); - - HikNvrPlaybackHlsSession resumed = service.control( - created.getSessionId(), - HikNvrPlaybackHlsControlRequest.builder() - .action(HikNvrPlaybackHlsControlAction.RESUME) - .build()); - - assertEquals(HikNvrPlaybackHlsStatus.PLAYING, resumed.getStatus()); - assertEquals(2, resumed.getGeneration()); - assertEquals("20260713124512", resumed.getCurrentTime()); - assertTrue(resumed.getHlsUrl().endsWith("-2/hls.m3u8")); - verify(handle).close(); - verify(replacement).setSpeed(1); - } - - @Test - public void recreatesPlaybackAtRequestedSpeedWhenSpeedCommandFindsDisconnectedSdkHandle() { - HikNvrPlaybackHlsSession created = service.create(request()); - HikSdkPlaybackHandle replacement = org.mockito.Mockito.mock(HikSdkPlaybackHandle.class); - when(handle.getLastPlaybackTime()).thenReturn(LocalDateTime.of(2026, 7, 13, 12, 45, 12)); - doThrow(HikNvrPlaybackException.sdkFailure( - HCNetSDK.NET_DVR_NETWORK_SEND_ERROR, - "连接已结束")) - .when(handle).setSpeed(8); - when(gateway.open(any(), any(LocalDateTime.class), any(LocalDateTime.class), any())) - .thenReturn(replacement); - - HikNvrPlaybackHlsSession changed = service.control( - created.getSessionId(), - HikNvrPlaybackHlsControlRequest.builder() - .action(HikNvrPlaybackHlsControlAction.SET_SPEED) - .speed(8) - .build()); - - assertEquals(HikNvrPlaybackHlsStatus.PLAYING, changed.getStatus()); - assertEquals(8, changed.getSpeed()); - assertEquals(2, changed.getGeneration()); - verify(handle).close(); - verify(replacement).setSpeed(8); - } - - @Test - public void recreatesPlaybackForSeekWithoutCallingTheExistingSdkHandle() { - HikNvrPlaybackHlsSession created = service.create(request()); - HikSdkPlaybackHandle replacement = org.mockito.Mockito.mock(HikSdkPlaybackHandle.class); - when(gateway.open(any(), any(LocalDateTime.class), any(LocalDateTime.class), any())) - .thenReturn(replacement); - - HikNvrPlaybackHlsSession moved = service.control( - created.getSessionId(), - HikNvrPlaybackHlsControlRequest.builder() - .action(HikNvrPlaybackHlsControlAction.SEEK) - .seekTime("2026-07-13 12:45:00") - .build()); - - assertEquals(HikNvrPlaybackHlsStatus.PLAYING, moved.getStatus()); - assertEquals(2, moved.getGeneration()); - assertEquals("20260713124500", moved.getCurrentTime()); - assertTrue(moved.getHlsUrl().endsWith("-2/hls.m3u8")); - verify(handle).close(); - verify(handle, never()).seek(any(LocalDateTime.class)); - verify(handle, never()).pause(); - verify(replacement).setSpeed(1); - } - - /** UI 暂停动作携带匹配 cameraCode 时应控制当前绑定会话。 */ - @Test - public void controlsUiSessionWhenCameraCodeMatches() { - // 创建已经绑定 cameraCode 的 UI 会话。 - HikNvrPlaybackHlsSession created = service.create(request()); - - HikNvrPlaybackHlsSession paused = service.controlByCameraCode( - created.getSessionId(), - HikNvrPlaybackHlsControlRequest.builder() - .cameraCode(CAMERA_CODE) - .action(HikNvrPlaybackHlsControlAction.PAUSE) - .build()); - - assertEquals(CAMERA_CODE, paused.getCameraCode()); - assertEquals(HikNvrPlaybackHlsStatus.PAUSED, paused.getStatus()); - verify(handle).pause(); - } - - /** UI 控制携带其他 cameraCode 时必须拒绝,不能操作旧摄像机会话。 */ - @Test - public void rejectsUiControlWhenCameraCodeDoesNotMatch() { - // 先创建目标会话,再模拟页面已经切换到其他摄像机。 - HikNvrPlaybackHlsSession created = service.create(request()); - try { - service.controlByCameraCode( - created.getSessionId(), - HikNvrPlaybackHlsControlRequest.builder() - .cameraCode("22078710632508667034#domain-a") - .action(HikNvrPlaybackHlsControlAction.PAUSE) - .build()); - fail("Expected HikNvrPlaybackException"); - } catch (HikNvrPlaybackException exception) { - assertEquals(HttpStatus.CONFLICT, exception.getStatus()); - assertTrue(exception.getMessage().contains("cameraCode")); - } - // 编码校验失败必须发生在调用海康 SDK 暂停命令之前。 - verify(handle, never()).pause(); - } - - private HikNvrPlaybackRequest request() { - // 内部请求显式绑定 cameraCode,供 UI 控制路径进行目标一致性校验。 - return HikNvrPlaybackRequest.builder() - .cameraCode(CAMERA_CODE) - .nvrIp("192.168.1.100") - .sdkPort(8000) - .rtspPort(554) - .channel(33) - .streamType(1) - .username("admin") - .password("123456") - .startTime("2026-07-13 12:00:00") - .endTime("2026-07-13 13:00:00") - .build(); - } - - private static final class TestProcessFactory extends HikFfmpegProcessFactory { - private final List> commands = new ArrayList<>(); - - @Override - public Process start(java.util.List command) { - commands.add(new ArrayList<>(command)); - return new TestProcess(); - } - - private String frameRateAt(int commandIndex) { - List command = commands.get(commandIndex); - int frameRateOption = command.indexOf("-framerate"); - return command.get(frameRateOption + 1); - } - } - - private static final class TestProcess extends Process { - private final ByteArrayOutputStream output = new ByteArrayOutputStream(); - - @Override - public OutputStream getOutputStream() { - return output; - } - - @Override - public InputStream getInputStream() { - return new ByteArrayInputStream(new byte[0]); - } - - @Override - public InputStream getErrorStream() { - return new ByteArrayInputStream(new byte[0]); - } - - @Override - public int waitFor() { - return 0; - } - - @Override - public boolean waitFor(long timeout, TimeUnit unit) { - return true; - } - - @Override - public int exitValue() { - return 0; - } - - @Override - public void destroy() { - } - - @Override - public Process destroyForcibly() { - return this; - } - } -} diff --git a/src/test/java/com/inspect/nvr/hik/service/HikNvrPlaybackServiceTest.java b/src/test/java/com/inspect/nvr/hik/service/HikNvrPlaybackServiceTest.java deleted file mode 100644 index a294c3bb..00000000 --- a/src/test/java/com/inspect/nvr/hik/service/HikNvrPlaybackServiceTest.java +++ /dev/null @@ -1,323 +0,0 @@ -package com.inspect.nvr.hik.service; - -import com.inspect.nvr.domain.Infrared.NvrInfo; -import com.inspect.nvr.hik.domain.HikNvrPlaybackRequest; -import com.inspect.nvr.hik.domain.HikNvrPlaybackResponse; -import com.inspect.nvr.hik.exception.HikNvrPlaybackException; -import com.inspect.nvr.hikVision.utils.jna.HCNetSDK; -import com.inspect.nvr.service.HikLoginService; -import com.inspect.nvr.service.HikLoginSession; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.junit.MockitoJUnitRunner; -import org.mockito.stubbing.Answer; -import org.springframework.test.util.ReflectionTestUtils; - -import java.time.LocalDateTime; -import java.util.Arrays; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@RunWith(MockitoJUnitRunner.class) -public class HikNvrPlaybackServiceTest { - - @Mock - private HikLoginService hikLoginService; - @Mock - private HCNetSDK hcNetSDK; - - private HikNvrPlaybackService service; - - @Before - public void setUp() { - service = new HikNvrPlaybackService(); - ReflectionTestUtils.setField(service, "hikLoginService", hikLoginService); - ReflectionTestUtils.setField(service, "hcNetSDK", hcNetSDK); - ReflectionTestUtils.setField(service, "urlBuilder", new HikPlaybackUrlBuilder()); - } - - @Test - public void returnsClippedMergedRangesAndKeepsRealGaps() { - prepareSearch(); - doAnswer(file("2026-07-13T12:20:00", "2026-07-13T12:40:00")) - .doAnswer(file("2026-07-13T12:30:00", "2026-07-13T12:50:00")) - .doAnswer(file("2026-07-13T12:55:00", "2026-07-13T13:10:00")) - .doReturn(HCNetSDK.NET_DVR_NOMOREFILE) - .when(hcNetSDK).NET_DVR_FindNextFile_V40( - eq(88), any(HCNetSDK.NET_DVR_FINDDATA_V40.class)); - - HikNvrPlaybackResponse response = service.search(request()); - - assertTrue(response.isHasRecord()); - assertEquals(2, response.getRecords().size()); - assertEquals(Arrays.asList("20260713122000", "20260713125000"), - response.getRecords().get(0).getTimeRange()); - assertEquals(Arrays.asList("20260713125500", "20260713130000"), - response.getRecords().get(1).getTimeRange()); - assertTrue(response.getRecords().get(1).getRtspUrl() - .contains("starttime=20260713T125500Z&endtime=20260713T130000Z")); - verify(hcNetSDK).NET_DVR_FindClose_V30(88); - - ArgumentCaptor conditionCaptor = - ArgumentCaptor.forClass(HCNetSDK.NET_DVR_FILECOND_V40.class); - verify(hcNetSDK).NET_DVR_FindFile_V40(eq(7), conditionCaptor.capture()); - HCNetSDK.NET_DVR_FILECOND_V40 condition = conditionCaptor.getValue(); - assertEquals(33, condition.lChannel); - assertEquals(0xff, condition.dwFileType); - assertEquals(0xff, condition.dwIsLocked); - assertEquals(3, condition.byStreamType); - assertEquals(12, condition.struStartTime.dwHour); - assertEquals(13, condition.struStopTime.dwHour); - } - - @Test - public void keepsSdkChannelThirtyThreeAndBuildsRtspTrackOne() { - prepareSearch(); - doAnswer(file("2026-07-13T12:00:00", "2026-07-13T13:00:00")) - .doReturn(HCNetSDK.NET_DVR_NOMOREFILE) - .when(hcNetSDK).NET_DVR_FindNextFile_V40( - eq(88), any(HCNetSDK.NET_DVR_FINDDATA_V40.class)); - HikNvrPlaybackRequest request = request(); - request.setChannel(33); - - HikNvrPlaybackResponse response = service.search(request); - - assertTrue(response.getRecords().get(0).getRtspUrl() - .contains("/Streaming/tracks/101?starttime=20260713T120000Z" - + "&endtime=20260713T130000Z")); - ArgumentCaptor conditionCaptor = - ArgumentCaptor.forClass(HCNetSDK.NET_DVR_FILECOND_V40.class); - verify(hcNetSDK).NET_DVR_FindFile_V40(eq(7), conditionCaptor.capture()); - assertEquals(33, conditionCaptor.getValue().lChannel); - } - - @Test - public void returnsEmptyResultWhenNvrHasNoRecordings() { - prepareSearch(); - when(hcNetSDK.NET_DVR_FindNextFile_V40( - eq(88), any(HCNetSDK.NET_DVR_FINDDATA_V40.class))) - .thenReturn(HCNetSDK.NET_DVR_FILE_NOFIND); - - HikNvrPlaybackResponse response = service.search(request()); - - assertFalse(response.isHasRecord()); - assertTrue(response.getRecords().isEmpty()); - verify(hcNetSDK).NET_DVR_FindClose_V30(88); - } - - @Test - public void mergesFilesThatMeetAtExactlyTheSameSecond() { - prepareSearch(); - doAnswer(file("2026-07-13T12:30:00", "2026-07-13T12:45:00")) - .doAnswer(file("2026-07-13T12:45:00", "2026-07-13T13:00:00")) - .doReturn(HCNetSDK.NET_DVR_NOMOREFILE) - .when(hcNetSDK).NET_DVR_FindNextFile_V40( - eq(88), any(HCNetSDK.NET_DVR_FINDDATA_V40.class)); - - HikNvrPlaybackResponse response = service.search(request()); - - assertEquals(1, response.getRecords().size()); - assertEquals(Arrays.asList("20260713123000", "20260713130000"), - response.getRecords().get(0).getTimeRange()); - } - - @Test - public void clipsRecordingThatStartsBeforeRequestedRange() { - prepareSearch(); - doAnswer(file("2026-07-13T11:50:00", "2026-07-13T12:30:00")) - .doReturn(HCNetSDK.NET_DVR_NOMOREFILE) - .when(hcNetSDK).NET_DVR_FindNextFile_V40( - eq(88), any(HCNetSDK.NET_DVR_FINDDATA_V40.class)); - - HikNvrPlaybackResponse response = service.search(request()); - - assertEquals(Arrays.asList("20260713120000", "20260713123000"), - response.getRecords().get(0).getTimeRange()); - } - - @Test - public void mapsSubStreamRequestToSdkSubStreamSearchType() { - prepareSearch(); - when(hcNetSDK.NET_DVR_FindNextFile_V40( - eq(88), any(HCNetSDK.NET_DVR_FINDDATA_V40.class))) - .thenReturn(HCNetSDK.NET_DVR_FILE_NOFIND); - HikNvrPlaybackRequest request = request(); - request.setStreamType(2); - - service.search(request); - - ArgumentCaptor conditionCaptor = - ArgumentCaptor.forClass(HCNetSDK.NET_DVR_FILECOND_V40.class); - verify(hcNetSDK).NET_DVR_FindFile_V40(eq(7), conditionCaptor.capture()); - assertEquals(1, conditionCaptor.getValue().byStreamType); - } - - @Test - public void retriesFindingStatusBeforeReturningRecord() { - prepareSearch(); - doReturn(HCNetSDK.NET_DVR_ISFINDING) - .doAnswer(file("2026-07-13T12:30:00", "2026-07-13T13:00:00")) - .doReturn(HCNetSDK.NET_DVR_NOMOREFILE) - .when(hcNetSDK).NET_DVR_FindNextFile_V40( - eq(88), any(HCNetSDK.NET_DVR_FINDDATA_V40.class)); - - HikNvrPlaybackResponse response = service.search(request()); - - assertTrue(response.isHasRecord()); - assertEquals(Arrays.asList("20260713123000", "20260713130000"), - response.getRecords().get(0).getTimeRange()); - verify(hcNetSDK).NET_DVR_FindClose_V30(88); - } - - @Test - public void propagatesSdkErrorAndStillClosesFindHandle() { - prepareSearch(); - when(hcNetSDK.NET_DVR_FindNextFile_V40( - eq(88), any(HCNetSDK.NET_DVR_FINDDATA_V40.class))) - .thenReturn(HCNetSDK.NET_DVR_FILE_EXCEPTION); - when(hcNetSDK.NET_DVR_GetLastError()).thenReturn(7); - - try { - service.search(request()); - fail("Expected HikNvrPlaybackException"); - } catch (HikNvrPlaybackException exception) { - assertEquals(7, exception.getCode()); - } - - verify(hcNetSDK).NET_DVR_FindClose_V30(88); - } - - @Test - public void returnsFindFileErrorWithoutClosingInvalidHandle() { - when(hikLoginService.loginSession(any(NvrInfo.class))).thenReturn(loginSession()); - when(hcNetSDK.NET_DVR_FindFile_V40( - eq(7), any(HCNetSDK.NET_DVR_FILECOND_V40.class))).thenReturn(-1); - when(hcNetSDK.NET_DVR_GetLastError()).thenReturn(4); - - try { - service.search(request()); - fail("Expected HikNvrPlaybackException"); - } catch (HikNvrPlaybackException exception) { - assertEquals(4, exception.getCode()); - } - - verify(hcNetSDK, never()).NET_DVR_FindClose_V30(anyInt()); - } - - @Test - public void wrapsLoginFailureWithCurrentSdkError() { - when(hikLoginService.loginSession(any(NvrInfo.class))) - .thenThrow(new RuntimeException("login failed")); - when(hcNetSDK.NET_DVR_GetLastError()).thenReturn(7); - - try { - service.search(request()); - fail("Expected HikNvrPlaybackException"); - } catch (HikNvrPlaybackException exception) { - assertEquals(7, exception.getCode()); - } - - verify(hcNetSDK, never()).NET_DVR_FindFile_V40( - anyInt(), any(HCNetSDK.NET_DVR_FILECOND_V40.class)); - } - - @Test - public void restoresInterruptFlagAndClosesHandleWhenWaitIsInterrupted() { - prepareSearch(); - when(hcNetSDK.NET_DVR_FindNextFile_V40( - eq(88), any(HCNetSDK.NET_DVR_FINDDATA_V40.class))) - .thenReturn(HCNetSDK.NET_DVR_ISFINDING); - - Thread.currentThread().interrupt(); - try { - service.search(request()); - fail("Expected HikNvrPlaybackException"); - } catch (HikNvrPlaybackException exception) { - assertEquals(-2, exception.getCode()); - assertTrue(Thread.currentThread().isInterrupted()); - } finally { - Thread.interrupted(); - } - - verify(hcNetSDK).NET_DVR_FindClose_V30(88); - } - - @Test - public void rejectsEndTimeBeforeStartWithoutLoggingIntoNvr() { - HikNvrPlaybackRequest request = request(); - request.setEndTime("2026-07-13 11:59:59"); - - try { - service.search(request); - fail("Expected HikNvrPlaybackException"); - } catch (HikNvrPlaybackException exception) { - assertEquals(-1, exception.getCode()); - } - - verify(hikLoginService, never()).loginSession(any(NvrInfo.class)); - verify(hcNetSDK, never()).NET_DVR_FindFile_V40( - anyInt(), any(HCNetSDK.NET_DVR_FILECOND_V40.class)); - } - - private void prepareSearch() { - when(hikLoginService.loginSession(any(NvrInfo.class))).thenReturn(loginSession()); - when(hcNetSDK.NET_DVR_FindFile_V40( - eq(7), any(HCNetSDK.NET_DVR_FILECOND_V40.class))).thenReturn(88); - when(hcNetSDK.NET_DVR_FindClose_V30(88)).thenReturn(true); - } - - private HikLoginSession loginSession() { - return new HikLoginSession(7, 0, 1, 8, 33); - } - - private HikNvrPlaybackRequest request() { - return HikNvrPlaybackRequest.builder() - .nvrIp("192.168.1.100") - .sdkPort(8000) - .rtspPort(554) - .channel(1) - .streamType(1) - .username("admin") - .password("123456") - .startTime("2026-07-13 12:00:00") - .endTime("2026-07-13 13:00:00") - .build(); - } - - private Answer file(String start, String end) { - LocalDateTime startTime = LocalDateTime.parse(start); - LocalDateTime endTime = LocalDateTime.parse(end); - return (InvocationOnMock invocation) -> { - HCNetSDK.NET_DVR_FINDDATA_V40 data = invocation.getArgument(1); - data.struStartTime = sdkTime(startTime); - data.struStopTime = sdkTime(endTime); - return HCNetSDK.NET_DVR_FILE_SUCCESS; - }; - } - - private HCNetSDK.NET_DVR_TIME sdkTime(LocalDateTime value) { - HCNetSDK.NET_DVR_TIME time = new HCNetSDK.NET_DVR_TIME(); - time.dwYear = value.getYear(); - time.dwMonth = value.getMonthValue(); - time.dwDay = value.getDayOfMonth(); - time.dwHour = value.getHour(); - time.dwMinute = value.getMinute(); - time.dwSecond = value.getSecond(); - return time; - } -} diff --git a/src/test/java/com/inspect/nvr/hik/service/HikNvrUiActionServiceTest.java b/src/test/java/com/inspect/nvr/hik/service/HikNvrUiActionServiceTest.java deleted file mode 100644 index d249a1d3..00000000 --- a/src/test/java/com/inspect/nvr/hik/service/HikNvrUiActionServiceTest.java +++ /dev/null @@ -1,337 +0,0 @@ -package com.inspect.nvr.hik.service; - -import com.inspect.nvr.hik.domain.HikNvrPtzMode; -import com.inspect.nvr.hik.domain.HikNvrUiActionRequest; -import com.inspect.nvr.hik.domain.HikNvrUiActionResponse; -import com.inspect.nvr.hikVision.utils.jna.HCNetSDK; -import com.inspect.nvr.domain.Infrared.NvrInfo; -import com.inspect.nvr.ivs.mapper.IvsCameraMapper; -import com.inspect.nvr.ivs.pojo.IvsCamera; -import com.inspect.nvr.service.HikCameraService; -import com.inspect.nvr.service.HikLoginService; -import com.inspect.nvr.service.HikLoginSession; -import com.sun.jna.Pointer; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.test.util.ReflectionTestUtils; - -import java.io.ByteArrayOutputStream; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.Map; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@RunWith(MockitoJUnitRunner.class) -public class HikNvrUiActionServiceTest { - - @Mock - private HikLoginService loginService; - @Mock - private HikCameraService cameraService; - @Mock - private HCNetSDK sdk; - @Mock - private IvsCameraMapper ivsCameraMapper; - - private HikNvrUiActionService service; - - @Before - public void setUp() { - // 构造完全来自数据库的 NVR 连接对象,服务不再依赖固定页面配置。 - NvrInfo databaseNvr = new NvrInfo(); - databaseNvr.setNvrName("测试 NVR"); - databaseNvr.setNvrIp("192.168.1.249"); - databaseNvr.setServerPort(8000); - databaseNvr.setRtspPort(554); - databaseNvr.setHttpPort(80); - databaseNvr.setAccount("admin"); - databaseNvr.setPassword("secret"); - databaseNvr.setStartChannel(1); - service = spy(new HikNvrUiActionService()); - ReflectionTestUtils.setField(service, "boundNvrInfo", databaseNvr); - ReflectionTestUtils.setField(service, "loginService", loginService); - ReflectionTestUtils.setField(service, "cameraService", cameraService); - ReflectionTestUtils.setField(service, "sdk", sdk); - ReflectionTestUtils.setField(service, "ivsCameraMapper", ivsCameraMapper); - // 清理静态通道状态,避免不同测试之间复用上一次 PTZ 动作的停止命令。 - Map channelStates = (Map) ReflectionTestUtils.getField( - HikNvrUiActionService.class, "PTZ_CHANNEL_STATES"); - if (channelStates != null) { - channelStates.clear(); - } - when(loginService.loginSession(any())).thenReturn( - new HikLoginSession(7, 0, 1, 8, 33)); - } - - @Test - public void callsSdkPresetWithoutIsapiRoundTrip() { - when(sdk.NET_DVR_PTZPreset_Other( - eq(7), eq(40), eq(HCNetSDK.GOTO_PRESET), eq(5))).thenReturn(true); - - HikNvrUiActionResponse response = service.preset(HikNvrUiActionRequest.builder() - .channel(8) - .preset(5) - .build()); - - assertTrue(response.isSuccess()); - assertEquals("已调用预置位 5(SDK)", response.getMessage()); - assertEquals(Integer.valueOf(40), response.getSdkChannel()); - verify(sdk).NET_DVR_PTZPreset_Other(7, 40, HCNetSDK.GOTO_PRESET, 5); - } - - @Test - public void sendsPtzThroughSdkWithConfiguredSpeed() { - when(sdk.NET_DVR_PTZControlWithSpeed_Other( - eq(7), eq(40), eq(HCNetSDK.PAN_LEFT), eq(0), eq(6))).thenReturn(true); - - HikNvrUiActionResponse response = service.ptz(HikNvrUiActionRequest.builder() - .channel(8) - .action("LEFT") - .speed(6) - .build()); - - assertTrue(response.isSuccess()); - assertEquals(Integer.valueOf(40), response.getSdkChannel()); - verify(sdk).NET_DVR_PTZControlWithSpeed_Other(7, 40, HCNetSDK.PAN_LEFT, 0, 6); - } - - @Test - public void sendsPointPtzStartAndAutoStopAfterOneSecond() throws Exception { - when(sdk.NET_DVR_PTZControlWithSpeed_Other( - eq(7), eq(40), eq(HCNetSDK.PAN_LEFT), eq(0), eq(6))).thenReturn(true); - when(sdk.NET_DVR_PTZControlWithSpeed_Other( - eq(7), eq(40), eq(HCNetSDK.PAN_LEFT), eq(1), eq(6))).thenReturn(true); - - HikNvrUiActionResponse response = service.ptz( - HikNvrUiActionRequest.builder() - .channel(8) - .action("LEFT") - .speed(6) - .build(), - HikNvrPtzMode.POINT); - - assertTrue(response.isSuccess()); - assertTrue(response.getMessage().contains("1秒后自动停止")); - verify(sdk).NET_DVR_PTZControlWithSpeed_Other(7, 40, HCNetSDK.PAN_LEFT, 0, 6); - verify(sdk, timeout(2500)).NET_DVR_PTZControlWithSpeed_Other( - 7, 40, HCNetSDK.PAN_LEFT, 1, 6); - } - - @Test - public void explicitStopCancelsPendingPointStop() throws Exception { - when(sdk.NET_DVR_PTZControlWithSpeed_Other( - eq(7), eq(40), eq(HCNetSDK.PAN_LEFT), eq(0), eq(6))).thenReturn(true); - when(sdk.NET_DVR_PTZControlWithSpeed_Other( - eq(7), eq(40), eq(HCNetSDK.PAN_LEFT), eq(1), eq(6))).thenReturn(true); - - service.ptz(HikNvrUiActionRequest.builder() - .channel(8) - .action("LEFT") - .speed(6) - .build(), HikNvrPtzMode.POINT); - service.ptz(HikNvrUiActionRequest.builder() - .channel(8) - .action("STOP") - .speed(6) - .build()); - - Thread.sleep(1200L); - verify(sdk, times(1)).NET_DVR_PTZControlWithSpeed_Other( - 7, 40, HCNetSDK.PAN_LEFT, 1, 6); - } - - /** 验证光圈动作会映射为海康光圈放大命令并携带设备速度。 */ - @Test - public void sendsLensCommandThroughSdkWithMappedCommand() { - when(sdk.NET_DVR_PTZControlWithSpeed_Other( - eq(7), eq(40), eq(HCNetSDK.IRIS_OPEN), eq(0), eq(7))).thenReturn(true); - - HikNvrUiActionResponse response = service.ptz(HikNvrUiActionRequest.builder() - .channel(8) - .action("IRIS_OPEN") - .speed(7) - .build()); - - assertTrue(response.isSuccess()); - verify(sdk).NET_DVR_PTZControlWithSpeed_Other( - 7, 40, HCNetSDK.IRIS_OPEN, 0, 7); - } - - /** 验证停止请求会复用最近一次启动的聚焦命令,而不是使用方向命令占位值。 */ - @Test - public void stopsThePreviouslyStartedLensCommand() { - when(sdk.NET_DVR_PTZControlWithSpeed_Other( - eq(7), eq(40), eq(HCNetSDK.FOCUS_NEAR), eq(0), eq(5))).thenReturn(true); - when(sdk.NET_DVR_PTZControlWithSpeed_Other( - eq(7), eq(40), eq(HCNetSDK.FOCUS_NEAR), eq(1), eq(5))).thenReturn(true); - - service.ptz(HikNvrUiActionRequest.builder() - .channel(8) - .action("FOCUS_NEAR") - .speed(5) - .build()); - HikNvrUiActionResponse response = service.ptz(HikNvrUiActionRequest.builder() - .channel(8) - .action("STOP") - .speed(4) - .build()); - - assertTrue(response.isSuccess()); - verify(sdk).NET_DVR_PTZControlWithSpeed_Other( - 7, 40, HCNetSDK.FOCUS_NEAR, 1, 5); - } - - /** 验证雨刷打开请求会调用海康辅助设备控制接口。 */ - @Test - public void controlsWiperThroughAuxiliarySdkCommand() { - when(sdk.NET_DVR_PTZControl_Other( - eq(7), eq(40), eq(HCNetSDK.WIPER_PWRON), eq(0))).thenReturn(true); - - HikNvrUiActionResponse response = service.auxiliaryControl(8, 1, true); - - assertTrue(response.isSuccess()); - verify(sdk).NET_DVR_PTZControl_Other(7, 40, HCNetSDK.WIPER_PWRON, 0); - } - - /** 验证起始位调用会构造远程控制结构体并发送给海康 SDK。 */ - @Test - public void callsConfiguredInitialPositionThroughRemoteControl() { - when(sdk.NET_DVR_RemoteControl( - eq(7), eq(HCNetSDK.NET_DVR_PTZ_INITIALPOSITIONCTRL), - any(Pointer.class), anyInt())).thenReturn(true); - - HikNvrUiActionResponse response = service.gotoInitialPosition(8); - - assertTrue(response.isSuccess()); - assertEquals(Integer.valueOf(40), response.getSdkChannel()); - verify(sdk).NET_DVR_RemoteControl( - eq(7), eq(HCNetSDK.NET_DVR_PTZ_INITIALPOSITIONCTRL), - any(Pointer.class), anyInt()); - } - - @Test - public void savesUnnamedPresetThroughSdkWithoutHttpNameSync() { - when(sdk.NET_DVR_PTZPreset_Other( - eq(7), eq(40), eq(HCNetSDK.SET_PRESET), eq(6))).thenReturn(true); - - HikNvrUiActionResponse response = service.createPreset(HikNvrUiActionRequest.builder() - .channel(8) - .preset(6) - .build()); - - assertTrue(response.isSuccess()); - assertEquals("已保存预置位 6(SDK)", response.getMessage()); - assertEquals(Integer.valueOf(40), response.getSdkChannel()); - verify(sdk).NET_DVR_PTZPreset_Other(7, 40, HCNetSDK.SET_PRESET, 6); - } - - @Test - public void savesPresetNameThroughCameraSdkAndNvrIsapi() { - when(sdk.NET_DVR_PTZPreset_Other( - eq(7), eq(40), eq(HCNetSDK.SET_PRESET), eq(6))).thenReturn(true); - when(sdk.NET_DVR_SetDVRConfig( - eq(7), eq(HCNetSDK.NET_DVR_SET_PRESET_NAME), eq(40), - any(Pointer.class), anyInt())).thenReturn(true); - doNothing().when(service).setPresetByIsapi(eq(8), eq(6), eq("测试大门")); - - HikNvrUiActionResponse response = service.createPreset(HikNvrUiActionRequest.builder() - .channel(8) - .preset(6) - .presetName("测试大门") - .build()); - - assertTrue(response.isSuccess()); - assertEquals("已保存预置位 6(SDK),名称:测试大门", response.getMessage()); - verify(sdk).NET_DVR_SetDVRConfig( - eq(7), eq(HCNetSDK.NET_DVR_SET_PRESET_NAME), eq(40), - any(Pointer.class), anyInt()); - } - - @Test - public void prefersClientDemoDirectCameraTargetWhenConfigured() { - // 直连目标只从当前 NVR 的 ivs_camera 记录读取。 - IvsCamera camera = new IvsCamera(); - camera.setCode("22078710632508667008#database-domain"); - camera.setDevIP("192.168.1.254"); - when(ivsCameraMapper.selectByNvrIp("192.168.1.249")) - .thenReturn(java.util.Collections.singletonList(camera)); - doReturn(true).when(service).isCameraReachable(any(IvsCamera.class)); - - when(loginService.loginSession(any(NvrInfo.class))).thenAnswer(invocation -> { - NvrInfo info = invocation.getArgument(0); - if ("192.168.1.254".equals(info.getNvrIp())) { - return new HikLoginSession(9, 1, 1, 0, 0); - } - return new HikLoginSession(7, 0, 1, 8, 33); - }); - when(sdk.NET_DVR_PTZPreset_Other( - eq(9), eq(1), eq(HCNetSDK.GOTO_PRESET), eq(5))).thenReturn(true); - - HikNvrUiActionResponse response = service.preset(HikNvrUiActionRequest.builder() - .channel(8) - .preset(5) - .build()); - - assertTrue(response.isSuccess()); - assertEquals(Integer.valueOf(1), response.getSdkChannel()); - verify(sdk).NET_DVR_PTZPreset_Other(9, 1, HCNetSDK.GOTO_PRESET, 5); - verify(loginService).loginSession(any(NvrInfo.class)); - } - - @Test - public void decodesPresetNameUsingItsActualEncoding() { - String utf8Name = ReflectionTestUtils.invokeMethod( - service, "decodePresetName", "正确".getBytes(StandardCharsets.UTF_8)); - String gbkName = ReflectionTestUtils.invokeMethod( - service, "decodePresetName", "预置点 3".getBytes(Charset.forName("GBK"))); - - assertEquals("正确", utf8Name); - assertEquals("预置点 3", gbkName); - } - - @Test - public void normalizesMixedPresetXmlWithoutChangingUtf8Names() throws Exception { - ByteArrayOutputStream xml = new ByteArrayOutputStream(); - xml.write(("" - + "1").getBytes(StandardCharsets.UTF_8)); - xml.write("正确".getBytes(StandardCharsets.UTF_8)); - xml.write(("3") - .getBytes(StandardCharsets.UTF_8)); - xml.write("预置点 3".getBytes(Charset.forName("GBK"))); - xml.write("" - .getBytes(StandardCharsets.UTF_8)); - - String normalized = ReflectionTestUtils.invokeMethod( - service, "normalizePresetXml", xml.toByteArray()); - - assertTrue(normalized.contains("正确")); - assertTrue(normalized.contains("预置点 3")); - } - - @Test - public void buildsPresetNamePayloadAsUtf8() { - String xml = ReflectionTestUtils.invokeMethod( - service, "buildPresetXml", 6, "测试大门"); - byte[] wireBytes = xml.getBytes(StandardCharsets.UTF_8); - - assertTrue(new String(wireBytes, StandardCharsets.UTF_8) - .contains("测试大门")); - assertEquals(-1, new String(wireBytes, Charset.forName("GBK")) - .indexOf("测试大门")); - } -} diff --git a/src/test/java/com/inspect/nvr/hik/service/HikPlaybackHlsUrlBuilderTest.java b/src/test/java/com/inspect/nvr/hik/service/HikPlaybackHlsUrlBuilderTest.java deleted file mode 100644 index 5f997a05..00000000 --- a/src/test/java/com/inspect/nvr/hik/service/HikPlaybackHlsUrlBuilderTest.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.inspect.nvr.hik.service; - -import org.junit.Test; -import org.springframework.test.util.ReflectionTestUtils; - -import static org.junit.Assert.assertEquals; - -public class HikPlaybackHlsUrlBuilderTest { - - @Test - public void buildsLocalHlsUrlForStreamGeneration() { - HikPlaybackHlsUrlBuilder builder = new HikPlaybackHlsUrlBuilder(); - ReflectionTestUtils.invokeMethod(builder, "initialize"); - - assertEquals("http://127.0.0.1:18081/playback/a1-2/hls.m3u8", - builder.hlsUrl("a1", 2)); - } -} diff --git a/src/test/java/com/inspect/nvr/hik/service/HikPlaybackUrlBuilderTest.java b/src/test/java/com/inspect/nvr/hik/service/HikPlaybackUrlBuilderTest.java deleted file mode 100644 index 08e5973b..00000000 --- a/src/test/java/com/inspect/nvr/hik/service/HikPlaybackUrlBuilderTest.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.inspect.nvr.hik.service; - -import com.inspect.nvr.hik.domain.HikNvrPlaybackRequest; -import org.junit.Test; - -import java.time.LocalDateTime; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -public class HikPlaybackUrlBuilderTest { - - private final HikPlaybackUrlBuilder builder = new HikPlaybackUrlBuilder(); - - @Test - public void buildsMainStreamPlaybackUrlUsingResolvedChannelAndBeijingTimes() { - HikNvrPlaybackRequest request = request("admin", "p@ss word", 33, 1); - - String url = builder.build( - request, - 1, - LocalDateTime.of(2026, 7, 13, 12, 30, 0), - LocalDateTime.of(2026, 7, 13, 13, 0, 0)); - - assertEquals("rtsp://admin:p%40ss%20word@192.168.1.100:554/Streaming/tracks/101" - + "?starttime=20260713T123000Z&endtime=20260713T130000Z", url); - } - - @Test - public void buildsSubStreamTrackFromResolvedRtspChannel() { - HikNvrPlaybackRequest request = request("operator", "secret", 33, 2); - - String url = builder.build( - request, - 1, - LocalDateTime.of(2026, 7, 13, 12, 0, 0), - LocalDateTime.of(2026, 7, 13, 13, 0, 0)); - - assertTrue(url.contains("/Streaming/tracks/102?")); - } - - @Test - public void buildsLiveUrlUsingTheSameResolvedChannelAndCredentials() { - HikNvrPlaybackRequest request = request("admin", "p@ss word", 33, 1); - - String url = builder.buildLive(request, 1); - - assertEquals("rtsp://admin:p%40ss%20word@192.168.1.100:554/Streaming/Channels/101", url); - } - - @Test - public void percentEncodesUnicodeAndReservedCredentialCharacters() { - HikNvrPlaybackRequest request = request("用:户", "p%/密", 1, 1); - - String url = builder.build( - request, - 1, - LocalDateTime.of(2026, 7, 13, 12, 0, 0), - LocalDateTime.of(2026, 7, 13, 13, 0, 0)); - - assertTrue(url.startsWith( - "rtsp://%E7%94%A8%3A%E6%88%B7:p%25%2F%E5%AF%86@192.168.1.100:554/")); - } - - private HikNvrPlaybackRequest request( - String username, - String password, - int channel, - int streamType) { - return HikNvrPlaybackRequest.builder() - .nvrIp("192.168.1.100") - .sdkPort(8000) - .rtspPort(554) - .channel(channel) - .streamType(streamType) - .username(username) - .password(password) - .startTime("2026-07-13 12:00:00") - .endTime("2026-07-13 13:00:00") - .build(); - } -} diff --git a/src/test/java/com/inspect/nvr/hik/service/HikSdkPlaybackGatewayTest.java b/src/test/java/com/inspect/nvr/hik/service/HikSdkPlaybackGatewayTest.java deleted file mode 100644 index 1f7bfea6..00000000 --- a/src/test/java/com/inspect/nvr/hik/service/HikSdkPlaybackGatewayTest.java +++ /dev/null @@ -1,314 +0,0 @@ -package com.inspect.nvr.hik.service; - -import com.inspect.nvr.dahua.DahuaNvrPlaybackService; -import com.inspect.nvr.domain.Infrared.NvrInfo; -import com.inspect.nvr.hik.domain.HikNvrPlaybackRequest; -import com.inspect.nvr.hik.exception.HikNvrPlaybackException; -import com.inspect.nvr.hikVision.utils.jna.HCNetSDK; -import com.inspect.nvr.service.HikLoginService; -import com.inspect.nvr.service.HikLoginSession; -import com.inspect.nvr.playback.NvrSdkPlaybackHandle; -import com.sun.jna.Memory; -import com.sun.jna.Pointer; -import com.sun.jna.ptr.IntByReference; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.test.util.ReflectionTestUtils; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.clearInvocations; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@RunWith(MockitoJUnitRunner.class) -public class HikSdkPlaybackGatewayTest { - - @Mock - private HikLoginService hikLoginService; - @Mock - private HCNetSDK sdk; - /** 大华回放服务模拟对象。 */ - @Mock - private DahuaNvrPlaybackService dahuaPlaybackService; - - private HikSdkPlaybackGateway gateway; - - @Before - public void setUp() { - gateway = new HikSdkPlaybackGatewayImpl(); - ReflectionTestUtils.setField(gateway, "hikLoginService", hikLoginService); - ReflectionTestUtils.setField(gateway, "sdk", sdk); - ReflectionTestUtils.setField(gateway, "dahuaPlaybackService", dahuaPlaybackService); - when(hikLoginService.loginSession(any(NvrInfo.class))) - .thenReturn(new HikLoginSession(7, 0, 1, 8, 33)); - when(sdk.NET_DVR_PlayBackByTime_V40(eq(7), any(HCNetSDK.NET_DVR_VOD_PARA.class))) - .thenReturn(91); - when(sdk.NET_DVR_SetPlayBackESCallBack(eq(91), any(HCNetSDK.FPlayESCallBack.class), isNull())) - .thenReturn(true); - when(sdk.NET_DVR_PlayBackControl_V40( - eq(91), anyInt(), any(), anyInt(), any(), any(IntByReference.class))) - .thenReturn(true); - } - - /** 数据库厂商为大华时,公共 HLS 网关必须直接返回 NetSDK 句柄。 */ - @Test - public void routesDahuaPlaybackWithoutCallingHikSdk() { - HikNvrPlaybackRequest request = request(); - request.setVendor("DAHUA"); - NvrSdkPlaybackHandle dahuaHandle = - org.mockito.Mockito.mock(NvrSdkPlaybackHandle.class); - when(dahuaPlaybackService.supports(request)).thenReturn(true); - when(dahuaPlaybackService.open(eq(request), eq(start()), eq(end()), any())) - .thenReturn(dahuaHandle); - - NvrSdkPlaybackHandle result = gateway.open( - request, start(), end(), frame -> true); - - assertEquals(dahuaHandle, result); - verify(dahuaPlaybackService).open(eq(request), eq(start()), eq(end()), any()); - verify(sdk, never()).NET_DVR_PlayBackByTime_V40( - anyInt(), any(HCNetSDK.NET_DVR_VOD_PARA.class)); - } - - @Test - public void exposesAbsoluteSeekControlAndStructuredPacketLayout() { - assertEquals(26, HCNetSDK.NET_DVR_PLAYSETTIME); - assertTrue(new HCNetSDK.NET_DVR_PACKET_INFO_EX().size() > 80); - } - - @Test - public void startsSdkPlaybackWithOriginalSdkChannelAndActualRange() { - gateway.open(request(), start(), end(), frame -> true); - - ArgumentCaptor vodCaptor = - ArgumentCaptor.forClass(HCNetSDK.NET_DVR_VOD_PARA.class); - verify(sdk).NET_DVR_PlayBackByTime_V40(eq(7), vodCaptor.capture()); - HCNetSDK.NET_DVR_VOD_PARA vod = vodCaptor.getValue(); - assertEquals(33, vod.struIDInfo.dwChannel); - assertEquals(0, vod.byStreamType); - assertEquals(12, vod.struBeginTime.dwHour); - assertEquals(13, vod.struEndTime.dwHour); - verify(sdk).NET_DVR_PlayBackControl_V40(eq(91), eq(HCNetSDK.NET_DVR_PLAYSTART), - isNull(), eq(0), isNull(), any(IntByReference.class)); - verify(sdk).NET_DVR_PlayBackControl_V40(eq(91), eq(HCNetSDK.NET_DVR_PLAYNORMAL), - isNull(), eq(0), isNull(), any(IntByReference.class)); - } - - @Test - public void explainsWhenNvrHasReachedItsMaximumClientConnections() { - when(sdk.NET_DVR_PlayBackByTime_V40(eq(7), any(HCNetSDK.NET_DVR_VOD_PARA.class))) - .thenReturn(-1); - when(sdk.NET_DVR_GetLastError()).thenReturn(HCNetSDK.NET_DVR_OVER_MAXLINK); - - try { - gateway.open(request(), start(), end(), frame -> true); - fail("Expected HikNvrPlaybackException"); - } catch (HikNvrPlaybackException exception) { - assertEquals(HCNetSDK.NET_DVR_OVER_MAXLINK, exception.getCode()); - assertTrue(exception.getMessage().contains("NVR客户端连接数已达到上限")); - } - } - - @Test - public void mapsLogicalChannelToSdkChannelWhenOpeningPlayback() { - HikNvrPlaybackRequest request = request(); - request.setChannel(1); - - gateway.open(request, start(), end(), frame -> true); - - ArgumentCaptor vodCaptor = - ArgumentCaptor.forClass(HCNetSDK.NET_DVR_VOD_PARA.class); - verify(sdk).NET_DVR_PlayBackByTime_V40(eq(7), vodCaptor.capture()); - assertEquals(33, vodCaptor.getValue().struIDInfo.dwChannel); - } - - @Test - public void mapsSpeedEightToNormalThenThreeFastCommands() { - HikSdkPlaybackHandle handle = (HikSdkPlaybackHandle) - gateway.open(request(), start(), end(), frame -> true); - clearInvocations(sdk); - - handle.setSpeed(8); - - verify(sdk, times(1)).NET_DVR_PlayBackControl_V40(eq(91), eq(HCNetSDK.NET_DVR_PLAYNORMAL), - isNull(), eq(0), isNull(), any(IntByReference.class)); - verify(sdk, times(3)).NET_DVR_PlayBackControl_V40(eq(91), eq(HCNetSDK.NET_DVR_PLAYFAST), - isNull(), eq(0), isNull(), any(IntByReference.class)); - } - - @Test - public void seeksUsingNetDvrTimePointer() { - HikSdkPlaybackHandle handle = (HikSdkPlaybackHandle) - gateway.open(request(), start(), end(), frame -> true); - - handle.seek(LocalDateTime.of(2026, 7, 13, 12, 45)); - - verify(sdk).NET_DVR_PlayBackControl_V40(eq(91), eq(HCNetSDK.NET_DVR_PLAYSETTIME), - any(Pointer.class), eq(new HCNetSDK.NET_DVR_TIME().size()), - isNull(), any(IntByReference.class)); - } - - @Test - public void forwardsAnnexBCodecHeaderBeforeVideoFrames() { - List frames = new ArrayList<>(); - HikSdkPlaybackHandle handle = (HikSdkPlaybackHandle) - gateway.open(request(), start(), end(), frames::add); - ArgumentCaptor callback = - ArgumentCaptor.forClass(HCNetSDK.FPlayESCallBack.class); - verify(sdk).NET_DVR_SetPlayBackESCallBack(eq(91), callback.capture(), isNull()); - - byte[] codecHeader = new byte[]{0, 0, 0, 1, 0x67, 0x42, 0, 0x1f}; - Memory memory = new Memory(codecHeader.length); - memory.write(0, codecHeader, 0, codecHeader.length); - HCNetSDK.NET_DVR_PACKET_INFO_EX packet = new HCNetSDK.NET_DVR_PACKET_INFO_EX(); - packet.dwPacketType = 0; - packet.dwPacketSize = codecHeader.length; - packet.pPacketBuffer = memory; - - callback.getValue().invoke(91, packet, null); - - assertEquals(1, frames.size()); - assertEquals(0, frames.get(0).getPacketType()); - - List nextSinkFrames = new ArrayList<>(); - handle.setFrameSink(nextSinkFrames::add); - - assertEquals(1, nextSinkFrames.size()); - assertEquals(0, nextSinkFrames.get(0).getPacketType()); - } - - @Test - public void replaysParameterSetsEmbeddedInAnIFrameWhenSinkChanges() { - List frames = new ArrayList<>(); - HikSdkPlaybackHandle handle = (HikSdkPlaybackHandle) - gateway.open(request(), start(), end(), frames::add); - ArgumentCaptor callback = - ArgumentCaptor.forClass(HCNetSDK.FPlayESCallBack.class); - verify(sdk).NET_DVR_SetPlayBackESCallBack(eq(91), callback.capture(), isNull()); - - byte[] iFrameWithParameters = new byte[]{ - 0, 0, 0, 1, 0x67, 0x42, 0, 0x1f, - 0, 0, 0, 1, 0x68, (byte) 0xce, 0x06, - 0, 0, 0, 1, 0x65, (byte) 0x88, (byte) 0x84}; - Memory memory = new Memory(iFrameWithParameters.length); - memory.write(0, iFrameWithParameters, 0, iFrameWithParameters.length); - HCNetSDK.NET_DVR_PACKET_INFO_EX packet = new HCNetSDK.NET_DVR_PACKET_INFO_EX(); - packet.dwPacketType = 1; - packet.dwPacketSize = iFrameWithParameters.length; - packet.pPacketBuffer = memory; - callback.getValue().invoke(91, packet, null); - - List nextSinkFrames = new ArrayList<>(); - handle.setFrameSink(nextSinkFrames::add); - - assertEquals(1, nextSinkFrames.size()); - assertEquals(0, nextSinkFrames.get(0).getPacketType()); - assertTrue(nextSinkFrames.get(0).getPayload().length < iFrameWithParameters.length); - } - - @Test - public void recordsSourcePlaybackTimeFromVideoPackets() { - HikSdkPlaybackHandle handle = (HikSdkPlaybackHandle) - gateway.open(request(), start(), end(), frame -> true); - ArgumentCaptor callback = - ArgumentCaptor.forClass(HCNetSDK.FPlayESCallBack.class); - verify(sdk).NET_DVR_SetPlayBackESCallBack(eq(91), callback.capture(), isNull()); - - byte[] payload = new byte[]{0, 0, 0, 1, 0x65}; - Memory memory = new Memory(payload.length); - memory.write(0, payload, 0, payload.length); - HCNetSDK.NET_DVR_PACKET_INFO_EX packet = new HCNetSDK.NET_DVR_PACKET_INFO_EX(); - packet.dwPacketType = 1; - packet.dwPacketSize = payload.length; - packet.pPacketBuffer = memory; - packet.dwYear = 2026; - packet.dwMonth = 7; - packet.dwDay = 13; - packet.dwHour = 12; - packet.dwMinute = 45; - packet.dwSecond = 12; - - callback.getValue().invoke(91, packet, null); - - assertEquals(LocalDateTime.of(2026, 7, 13, 12, 45, 12), handle.getLastPlaybackTime()); - } - - @Test - public void pacesNativePlaybackOutsideEsCallbackWhenSourceTimeRunsAhead() throws Exception { - HikSdkPlaybackHandle handle = (HikSdkPlaybackHandle) - gateway.open(request(), start(), end(), frame -> true); - ArgumentCaptor callback = - ArgumentCaptor.forClass(HCNetSDK.FPlayESCallBack.class); - verify(sdk).NET_DVR_SetPlayBackESCallBack(eq(91), callback.capture(), isNull()); - - byte[] payload = new byte[]{0, 0, 0, 1, 0x65}; - Memory memory = new Memory(payload.length); - memory.write(0, payload, 0, payload.length); - HCNetSDK.NET_DVR_PACKET_INFO_EX packet = new HCNetSDK.NET_DVR_PACKET_INFO_EX(); - packet.dwPacketType = 1; - packet.dwPacketSize = payload.length; - packet.pPacketBuffer = memory; - setPacketTime(packet, LocalDateTime.of(2026, 7, 13, 12, 0)); - callback.getValue().invoke(91, packet, null); - setPacketTime(packet, LocalDateTime.of(2026, 7, 13, 12, 0, 10)); - callback.getValue().invoke(91, packet, null); - - try { - Thread.sleep(250L); - verify(sdk, atLeastOnce()).NET_DVR_PlayBackControl_V40( - eq(91), eq(HCNetSDK.NET_DVR_PLAYPAUSE), - isNull(), eq(0), isNull(), any(IntByReference.class)); - } finally { - handle.close(); - } - } - - private HikNvrPlaybackRequest request() { - return HikNvrPlaybackRequest.builder() - .nvrIp("192.168.1.100") - .sdkPort(8000) - .rtspPort(554) - .channel(33) - .streamType(1) - .username("admin") - .password("123456") - .startTime("2026-07-13 12:00:00") - .endTime("2026-07-13 13:00:00") - .build(); - } - - private LocalDateTime start() { - return LocalDateTime.of(2026, 7, 13, 12, 0); - } - - private LocalDateTime end() { - return LocalDateTime.of(2026, 7, 13, 13, 0); - } - - private void setPacketTime(HCNetSDK.NET_DVR_PACKET_INFO_EX packet, LocalDateTime time) { - packet.dwYear = time.getYear(); - packet.dwMonth = time.getMonthValue(); - packet.dwDay = time.getDayOfMonth(); - packet.dwHour = time.getHour(); - packet.dwMinute = time.getMinute(); - packet.dwSecond = time.getSecond(); - packet.dwMillisecond = time.getNano() / 1_000_000; - } -} diff --git a/src/test/java/com/inspect/nvr/hik/ui/HikNvrPlaybackPageContractTest.java b/src/test/java/com/inspect/nvr/hik/ui/HikNvrPlaybackPageContractTest.java deleted file mode 100644 index db43c079..00000000 --- a/src/test/java/com/inspect/nvr/hik/ui/HikNvrPlaybackPageContractTest.java +++ /dev/null @@ -1,176 +0,0 @@ -package com.inspect.nvr.hik.ui; - -import org.junit.Test; -import org.springframework.util.StreamUtils; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -/** - * 校验历史回放静态页面与一分钟录像下载接口之间的关键集成约定。 - */ -public class HikNvrPlaybackPageContractTest { - - /** Spring Boot 历史回放页面在测试类路径中的资源位置。 */ - private static final String PLAYBACK_PAGE_RESOURCE = - "/static/playback.html"; - /** Spring Boot 监控首页在测试类路径中的资源位置。 */ - private static final String MONITOR_PAGE_RESOURCE = - "/static/index.html"; - - /** - * 页面必须向用户提供目标时刻输入和一分钟下载按钮。 - * - * @throws IOException 静态页面无法读取时抛出 - */ - @Test - public void shouldExposeOneMinuteDownloadControls() throws IOException { - // 读取最终随JAR发布的历史回放页面,避免只验证未打包的开发文件。 - String page = readPlaybackPage(); - - // 控件标识和提示文本共同保证用户能够在顶部查询区找到下载入口。 - assertTrue(page.contains("id=\"clipTime\"")); - assertTrue(page.contains("id=\"downloadClipBtn\"")); - assertTrue(page.contains("id=\"downloadClipText\"")); - assertTrue(page.contains("下载1分钟")); - assertTrue(page.contains("前30秒和后30秒")); - // 下载控件必须出现在视频舞台之前,避免再次隐藏到播放器底部。 - assertTrue(page.indexOf("id=\"clipTime\"") - < page.indexOf("class=\"stage-card\"")); - } - - /** - * 页面下载请求必须复用共用设备树选择,并固定提交前后各三十秒。 - * - * @throws IOException 静态页面无法读取时抛出 - */ - @Test - public void shouldReuseSelectionAndSubmitThirtySecondsOnEachSide() - throws IOException { - // 读取页面脚本并核对接口地址、设备选择和前后秒数的稳定契约。 - String page = readPlaybackPage(); - - assertTrue(page.contains( - "const CLIP_API = '/hik/nvr/playback/clips/download'")); - assertTrue(page.contains("cameraCode: requestedCameraCode")); - assertTrue(page.contains( - "const requestedCameraCode = String(")); - assertTrue(page.contains( - "const selectedNvrConfig = selectedNvr()")); - assertFalse(page.contains("nvrIp: requestedNvrIp")); - assertFalse(page.contains("channel: requestedChannel")); - assertTrue(page.contains("beforeSeconds: CLIP_CONTEXT_SECONDS")); - assertTrue(page.contains("afterSeconds: CLIP_CONTEXT_SECONDS")); - assertTrue(page.contains("const CLIP_CONTEXT_SECONDS = 30")); - // 目标输入留空时必须能够使用当前回放时刻,支持边看边下载。 - assertTrue(page.contains( - "state.session && state.session.currentTime")); - } - - /** - * 回放创建、暂停、恢复和跳转必须共同提交当前完整 cameraCode。 - * - * @throws IOException 静态页面无法读取时抛出 - */ - @Test - public void shouldUseCameraCodeForPlaybackSessionAndControls() - throws IOException { - // 读取页面脚本,核对创建与控制均绑定同一个页面 cameraCode 状态。 - String page = readPlaybackPage(); - - assertTrue(page.contains("cameraCode: requestedCameraCode")); - assertTrue(page.contains( - "/sessions/${encodeURIComponent(state.session.sessionId)}/controls")); - assertTrue(page.contains("cameraCode: state.selectedCameraCode")); - assertTrue(page.contains("cameraCode: boundCameraCode")); - assertTrue(page.contains("action: 'STOP'")); - assertTrue(page.contains("url.searchParams.set('cameraCode'")); - assertFalse(page.contains("url.searchParams.set('nvrIp'")); - assertFalse(page.contains("url.searchParams.set('channel'")); - } - - /** - * 监控首页的实时、云台和抓图必须携带 cameraCode 并复用现有 IVS 地址。 - * - * @throws IOException 静态页面无法读取时抛出 - */ - @Test - public void shouldRouteLivePtzAndSnapshotByCameraCodeThroughIvsContracts() - throws IOException { - // 读取最终首页,确认设备树标识和现有 IVS 能力地址已连通。 - String page = readResource(MONITOR_PAGE_RESOURCE, "监控首页必须存在"); - - assertTrue(page.contains("data-camera-code")); - assertTrue(page.contains("cameraCode: targetCameraCode")); - assertTrue(page.contains("const IVS_PTZ_API = '/device/ptzcontrol'")); - assertTrue(page.contains( - "const IVS_PRESET_LIST_API = '/device/ptzpresetlist'")); - assertTrue(page.contains( - "const IVS_SNAPSHOT_API = '/platform/platformSnapshot'")); - assertTrue(page.contains("cameraCode,")); - assertTrue(page.contains("data.ptzPresetInfoList.ptzPresetInfo")); - assertTrue(page.contains("item.presetIndex")); - assertTrue(page.contains("item.presetName")); - assertFalse(page.contains("API + '/actions/ptz'")); - assertFalse(page.contains("API + '/actions/snapshot'")); - assertFalse(page.contains("API + '/actions/presets?nvrIp='")); - } - - /** - * 页面必须用弹窗提示未来时间、无录像和录像不完整等下载错误。 - * - * @throws IOException 静态页面无法读取时抛出 - */ - @Test - public void shouldShowDialogForFutureOrMissingRecordErrors() - throws IOException { - // 读取页面并校验错误弹窗结构和未来时间拦截逻辑。 - String page = readPlaybackPage(); - - assertTrue(page.contains("id=\"clipErrorBackdrop\"")); - assertTrue(page.contains("role=\"alertdialog\"")); - assertTrue(page.contains("function showClipError(")); - assertTrue(page.contains("function isFutureClipCenter(")); - assertTrue(page.contains("目标时间尚未到达")); - assertTrue(page.contains("目标时间没有录像")); - assertTrue(page.contains("目标时间录像不完整")); - } - - /** - * 以UTF-8读取历史回放页面,确保中文提示不会在测试过程中发生编码替换。 - * - * @return 完整HTML文本 - * @throws IOException 页面资源读取失败时抛出 - */ - private String readPlaybackPage() throws IOException { - // 使用统一资源读取方法并保留历史回放页面的明确缺失提示。 - return readResource( - PLAYBACK_PAGE_RESOURCE, "历史回放页面必须存在"); - } - - /** - * 以 UTF-8 读取一个随 JAR 发布的静态页面资源。 - * - * @param resourcePath 类路径资源位置 - * @param missingMessage 资源缺失时的断言提示 - * @return 完整 HTML 文本 - * @throws IOException 页面资源读取失败时抛出 - */ - private String readResource( - String resourcePath, String missingMessage) throws IOException { - // 从类加载器取得 Maven 已复制到 target/classes 的静态资源。 - InputStream inputStream = getClass().getResourceAsStream(resourcePath); - // 页面未进入类路径时立即失败,避免后续断言给出误导性的空指针错误。 - assertNotNull(missingMessage, inputStream); - try (InputStream closableInputStream = inputStream) { - // 严格按项目统一的 UTF-8 读取页面内容供契约断言使用。 - return StreamUtils.copyToString( - closableInputStream, StandardCharsets.UTF_8); - } - } -} diff --git a/src/test/java/com/inspect/nvr/ivs/IvsCameraSyncServiceTest.java b/src/test/java/com/inspect/nvr/ivs/IvsCameraSyncServiceTest.java deleted file mode 100644 index 4e9df5ca..00000000 --- a/src/test/java/com/inspect/nvr/ivs/IvsCameraSyncServiceTest.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.inspect.nvr.ivs; - -import com.inspect.nvr.ivs.mapper.IvsCameraMapper; -import com.inspect.nvr.ivs.mapper.IvsNvrMapper; -import com.inspect.nvr.ivs.pojo.IvsCamera; -import com.inspect.nvr.ivs.pojo.IvsNvr; -import com.inspect.nvr.ivs.service.IvsCameraSyncService; -import com.inspect.nvr.ivs.util.IvsNvrChannelClient; -import com.inspect.nvr.ivs.vo.IvsCameraSyncResult; - -import org.junit.Test; -import org.springframework.test.util.ReflectionTestUtils; - -import java.util.Collections; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class IvsCameraSyncServiceTest { - - @Test - public void syncUsesNvrCodeSuffixAndThreeDigitChannelCode() { - IvsNvrMapper nvrMapper = mock(IvsNvrMapper.class); - IvsCameraMapper cameraMapper = mock(IvsCameraMapper.class); - IvsNvrChannelClient channelClient = mock(IvsNvrChannelClient.class); - - IvsNvr nvr = nvr("05199871611126810#d4b895b7110b4e6ca1dbdf30ed41826f"); - when(nvrMapper.selectAll()).thenReturn(Collections.singletonList(nvr)); - when(channelClient.listChannels(nvr)).thenReturn(Collections.singletonList( - new IvsNvrChannelClient.Channel(1, "Camera 01", "HIKVISION", - "192.168.1.244", 8000, 1, true))); - - IvsCameraSyncService service = new IvsCameraSyncService(); - ReflectionTestUtils.setField(service, "ivsNvrMapper", nvrMapper); - ReflectionTestUtils.setField(service, "ivsCameraMapper", cameraMapper); - ReflectionTestUtils.setField(service, "channelClient", channelClient); - IvsCameraSyncResult result = service.syncAll(); - - assertEquals(1, result.getSuccessNvrCount()); - assertEquals(1, result.getCameraCount()); - assertTrue(result.getErrors().isEmpty()); - org.mockito.ArgumentCaptor captor = org.mockito.ArgumentCaptor.forClass(IvsCamera.class); - verify(cameraMapper).upsert(captor.capture()); - IvsCamera camera = captor.getValue(); - assertEquals("05199871611126810001#d4b895b7110b4e6ca1dbdf30ed41826f", camera.getCode()); - assertEquals("05199871611126810000", camera.getParentCode()); - assertEquals("0#d4b895b7110b4e6ca1dbdf30ed41826f", camera.getDevGroupCode()); - assertEquals("d4b895b7110b4e6ca1dbdf30ed41826f", camera.getDomainCode()); - assertEquals(Integer.valueOf(1), camera.getCameraStatus()); - // 同步发现的实际通道数必须回填 NVR 台账,供大华整机录像状态数组使用。 - verify(nvrMapper).updateChannelCount(7L, 1); - assertEquals(Integer.valueOf(1), nvr.getChannelCount()); - verify(nvrMapper, never()).updateNvrCodeIfAbsent(any(Long.class), any(String.class)); - } - - @Test - public void syncGeneratesAndPersistsMissingNvrCode() { - IvsNvrMapper nvrMapper = mock(IvsNvrMapper.class); - IvsCameraMapper cameraMapper = mock(IvsCameraMapper.class); - IvsNvrChannelClient channelClient = mock(IvsNvrChannelClient.class); - - IvsNvr nvr = nvr(null); - when(nvrMapper.selectAll()).thenReturn(Collections.singletonList(nvr)); - when(nvrMapper.updateNvrCodeIfAbsent(eq(7L), any(String.class))).thenReturn(1); - when(channelClient.listChannels(any(IvsNvr.class))).thenReturn(Collections.singletonList( - new IvsNvrChannelClient.Channel(12, null, "HIKVISION", "10.0.0.12", - 8000, 1, false))); - - IvsCameraSyncService service = new IvsCameraSyncService(); - ReflectionTestUtils.setField(service, "ivsNvrMapper", nvrMapper); - ReflectionTestUtils.setField(service, "ivsCameraMapper", cameraMapper); - ReflectionTestUtils.setField(service, "channelClient", channelClient); - IvsCameraSyncResult result = service.syncAll(); - - assertEquals(1, result.getSuccessNvrCount()); - assertEquals(1, result.getCameraCount()); - assertNotNull(nvr.getNvrCode()); - assertTrue(nvr.getNvrCode().matches("\\d{17}#[0-9a-f]{32}")); - org.mockito.ArgumentCaptor captor = org.mockito.ArgumentCaptor.forClass(IvsCamera.class); - verify(cameraMapper).upsert(captor.capture()); - assertTrue(captor.getValue().getCode().matches("\\d{17}012#[0-9a-f]{32}")); - assertEquals(Integer.valueOf(0), captor.getValue().getStatus()); - } - - private IvsNvr nvr(String nvrCode) { - IvsNvr nvr = new IvsNvr(); - nvr.setNvrId(7L); - nvr.setNvrIp("192.168.1.249"); - nvr.setHttpPort(80); - nvr.setUsername("admin"); - nvr.setPassword("password"); - nvr.setVendor("HIKVISION"); - nvr.setNvrCode(nvrCode); - return nvr; - } -} diff --git a/src/test/java/com/inspect/nvr/ivs/IvsOpenApiServiceTest.java b/src/test/java/com/inspect/nvr/ivs/IvsOpenApiServiceTest.java deleted file mode 100644 index 8cadd81c..00000000 --- a/src/test/java/com/inspect/nvr/ivs/IvsOpenApiServiceTest.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.inspect.nvr.ivs; - -import com.inspect.nvr.domain.ivs.IvsChanSnapVo; -import com.inspect.nvr.domain.ivs.IvsDevChanSnapVo; -import com.inspect.nvr.domain.ivs.IvsSnapshotCommandResponse; -import com.inspect.nvr.ivs.mapper.IvsNvrMapper; -import com.inspect.nvr.ivs.pojo.IvsNvr; -import com.inspect.nvr.ivs.service.IvsOpenApiService; -import com.inspect.nvr.ivs.service.IvsVendorAdapter; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.test.util.ReflectionTestUtils; - -import java.util.Arrays; -import java.util.Collections; - -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** 验证混合厂商部署中的任务和句柄路由。 */ -@RunWith(MockitoJUnitRunner.class) -public class IvsOpenApiServiceTest { - - /** 模拟 NVR 台账查询。 */ - @Mock - private IvsNvrMapper nvrMapper; - /** 模拟海康厂商适配器。 */ - @Mock - private IvsVendorAdapter hikAdapter; - /** 模拟大华厂商适配器。 */ - @Mock - private IvsVendorAdapter dahuaAdapter; - /** 待验证的统一 IVS 门面。 */ - private IvsOpenApiService service; - - /** 注册海康和大华适配器,并配置一个大华 NVR 域。 */ - @Before - public void setUp() { - when(hikAdapter.vendorCode()).thenReturn("HIKVISION"); - when(dahuaAdapter.vendorCode()).thenReturn("DAHUA"); - IvsNvr nvr = new IvsNvr(); - nvr.setVendor("DAHUA"); - nvr.setNvrCode("32078710632508667#domain-a"); - when(nvrMapper.selectAll()).thenReturn(Collections.singletonList(nvr)); - service = new IvsOpenApiService(); - ReflectionTestUtils.setField(service, "nvrMapper", nvrMapper); - ReflectionTestUtils.setField( - service, "vendorAdapters", Arrays.asList(hikAdapter, dahuaAdapter)); - service.initialize(); - } - - /** 抓图创建后只传 taskID 查询列表仍必须命中原大华适配器。 */ - @Test - public void routesSnapshotListByRememberedTaskVendor() { - IvsDevChanSnapVo create = IvsDevChanSnapVo.builder() - .cameraCode("32078710632508667003") - .domainCode("domain-a") - .build(); - when(dahuaAdapter.platformSnapshot(create)).thenReturn( - IvsSnapshotCommandResponse.builder().resultCode(0).taskID("task-dh").build()); - service.platformSnapshot(create); - - IvsChanSnapVo query = IvsChanSnapVo.builder().taskID("task-dh").build(); - service.snapshotList(query); - - verify(dahuaAdapter).snapshotList(query); - verify(hikAdapter, never()).snapshotList(any(IvsChanSnapVo.class)); - } - - /** 携带大华 cameraCode 的停止请求不能再把原生句柄交给海康 SDK。 */ - @Test - public void routesPlaybackStopByCameraVendor() { - when(dahuaAdapter.stopPlatformPlayback(99L)).thenReturn(0); - - int result = service.stopPlatformPlayback( - 99L, "32078710632508667003", "domain-a"); - - assertEquals(0, result); - verify(dahuaAdapter).stopPlatformPlayback(99L); - verify(hikAdapter, never()).stopPlatformPlayback(99L); - } -} diff --git a/src/test/java/com/inspect/nvr/ivs/util/IvsNvrChannelClientTest.java b/src/test/java/com/inspect/nvr/ivs/util/IvsNvrChannelClientTest.java deleted file mode 100644 index 49731318..00000000 --- a/src/test/java/com/inspect/nvr/ivs/util/IvsNvrChannelClientTest.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.inspect.nvr.ivs.util; - -import com.inspect.nvr.dahua.DahuaNvrChannelClient; -import com.inspect.nvr.ivs.pojo.IvsNvr; - -import org.junit.Test; -import org.springframework.test.util.ReflectionTestUtils; - -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertSame; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** - * 验证 NVR ISAPI 响应的 UTF-8 解码规则,防止自动同步写入乱码摄像机名称。 - */ -public class IvsNvrChannelClientTest { - - /** - * vendor 为 Dahua 时必须转交大华 NetSDK 客户端,不能再执行海康 ISAPI 请求。 - */ - @Test - public void routesDahuaVendorToDahuaClient() { - // 构造大华台账记录和预期通道,模拟同步接口从数据库读取 vendor=Dahua。 - IvsNvr nvr = new IvsNvr(); - nvr.setVendor("Dahua"); - IvsNvrChannelClient.Channel expected = new IvsNvrChannelClient.Channel( - 1, "大华通道", "DAHUA", "192.168.1.20", 37777, 1, true); - DahuaNvrChannelClient dahuaClient = mock(DahuaNvrChannelClient.class); - when(dahuaClient.listChannels(nvr)).thenReturn(Collections.singletonList(expected)); - - // 注入大华实现后调用统一入口,验证厂商路由不改变同步服务的调用方式。 - IvsNvrChannelClient client = new IvsNvrChannelClient(); - ReflectionTestUtils.setField(client, "dahuaChannelClient", dahuaClient); - List channels = client.listChannels(nvr); - - // 统一入口应原样返回大华实现的结果,且实际调用一次 NetSDK 客户端。 - assertEquals(1, channels.size()); - assertSame(expected, channels.get(0)); - verify(dahuaClient).listChannels(nvr); - } - - /** - * UTF-8 中文名称必须保持原文,不能再按“汉字数量”误选 GBK 解码结果。 - */ - @Test - public void decodeKeepsUtf8CameraName() { - // 构造包含现场常见中文摄像机名称的 UTF-8 XML 响应。 - String xml = "" - + "大门入口摄像机"; - - // 调用实际响应解码方法,验证传给 XML 解析器的文本没有发生二次转码。 - String decoded = ReflectionTestUtils.invokeMethod( - new IvsNvrChannelClient(), "decode", xml.getBytes(StandardCharsets.UTF_8)); - - // 完整 XML 应逐字保持一致,从而保证摄像机名称原样写入 ivs_camera.name。 - assertEquals(xml, decoded); - } - - /** - * 非 UTF-8 字节必须明确失败,避免使用替换字符或错误编码污染摄像机名称。 - */ - @Test(expected = IllegalStateException.class) - public void decodeRejectsNonUtf8Response() { - // 使用 GBK 构造非法 UTF-8 响应,模拟设备或链路违反统一编码约定。 - byte[] bytes = "大门入口摄像机".getBytes(Charset.forName("GBK")); - - // 严格解码应抛出异常,由同步任务记录具体 NVR,而不是继续保存乱码。 - ReflectionTestUtils.invokeMethod(new IvsNvrChannelClient(), "decode", bytes); - } -} diff --git a/src/test/java/com/inspect/nvr/service/HikLoginServiceTest.java b/src/test/java/com/inspect/nvr/service/HikLoginServiceTest.java deleted file mode 100644 index 96eef88f..00000000 --- a/src/test/java/com/inspect/nvr/service/HikLoginServiceTest.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.inspect.nvr.service; - -import com.inspect.nvr.domain.Infrared.NvrInfo; -import com.inspect.nvr.hikVision.utils.jna.HCNetSDK; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.test.util.ReflectionTestUtils; - -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - -@RunWith(MockitoJUnitRunner.class) -public class HikLoginServiceTest { - - @Mock - private HCNetSDK hcNetSDK; - - private HikLoginService service; - - @Before - public void setUp() { - service = new HikLoginService(); - ReflectionTestUtils.setField(service, "hcNetSDK", hcNetSDK); - } - - @Test - public void capturesDeviceChannelLayoutInLoginSession() { - when(hcNetSDK.NET_DVR_Login_V40( - any(HCNetSDK.NET_DVR_USER_LOGIN_INFO.class), - any(HCNetSDK.NET_DVR_DEVICEINFO_V40.class))) - .thenAnswer(invocation -> { - HCNetSDK.NET_DVR_DEVICEINFO_V40 deviceInfo = invocation.getArgument(1); - deviceInfo.struDeviceV30.byChanNum = 0; - deviceInfo.struDeviceV30.byStartChan = 1; - deviceInfo.struDeviceV30.byIPChanNum = 8; - deviceInfo.struDeviceV30.byHighDChanNum = 0; - deviceInfo.struDeviceV30.byStartDChan = 33; - deviceInfo.write(); - return 7; - }); - - HikLoginSession session = service.loginSession(nvrInfo()); - - assertEquals(7, session.getUserId()); - assertEquals(1, session.toRtspChannel(33)); - assertEquals(2, session.toRtspChannel(34)); - } - - private NvrInfo nvrInfo() { - NvrInfo nvrInfo = new NvrInfo(); - nvrInfo.setNvrIp("192.168.1.249"); - nvrInfo.setServerPort(8000); - nvrInfo.setAccount("admin"); - nvrInfo.setPassword("secret"); - return nvrInfo; - } -} diff --git a/src/test/java/com/inspect/nvr/service/HikLoginSessionTest.java b/src/test/java/com/inspect/nvr/service/HikLoginSessionTest.java deleted file mode 100644 index e9976074..00000000 --- a/src/test/java/com/inspect/nvr/service/HikLoginSessionTest.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.inspect.nvr.service; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -public class HikLoginSessionTest { - - @Test - public void mapsPureNvrDigitalChannelsToOneBasedRtspChannels() { - HikLoginSession session = new HikLoginSession(7, 0, 1, 8, 33); - - assertEquals(1, session.toRtspChannel(33)); - assertEquals(2, session.toRtspChannel(34)); - } - - @Test - public void placesHybridDigitalChannelsAfterAnalogChannels() { - HikLoginSession session = new HikLoginSession(7, 16, 1, 8, 33); - - assertEquals(17, session.toRtspChannel(33)); - } - - @Test - public void normalizesAnalogSdkChannelsToOneBasedRtspChannels() { - HikLoginSession session = new HikLoginSession(7, 4, 5, 0, 0); - - assertEquals(1, session.toRtspChannel(5)); - assertEquals(4, session.toRtspChannel(8)); - } - - @Test - public void mapsLogicalChannelsToSdkChannelsWhileKeepingExplicitSdkChannels() { - HikLoginSession session = new HikLoginSession(7, 0, 1, 8, 33); - - assertEquals(33, session.toSdkChannel(1)); - assertEquals(40, session.toSdkChannel(8)); - assertEquals(33, session.toSdkChannel(33)); - } - - @Test - public void keepsOriginalChannelWhenDeviceLayoutDoesNotCoverIt() { - HikLoginSession session = new HikLoginSession(7, 0, 0, 8, 33); - - assertEquals(99, session.toRtspChannel(99)); - } - -}