Browse Source

chore: 停止跟踪测试源码

master
王寅 1 month ago
parent
commit
ebc895e70a
30 changed files with 3 additions and 4521 deletions
  1. +3
    -0
      .gitignore
  2. +0
    -112
      src/test/java/com/inspect/nvr/controller/IvsServerControllerTest.java
  3. +0
    -87
      src/test/java/com/inspect/nvr/dahua/DahuaIvsVendorServiceTest.java
  4. +0
    -156
      src/test/java/com/inspect/nvr/dahua/DahuaNvrChannelClientTest.java
  5. +0
    -148
      src/test/java/com/inspect/nvr/dahua/DahuaNvrPlaybackServiceTest.java
  6. +0
    -35
      src/test/java/com/inspect/nvr/domain/ivs/IvsSnapshotCommandResponseTest.java
  7. +0
    -179
      src/test/java/com/inspect/nvr/hik/controller/HikNvrPlaybackControllerTest.java
  8. +0
    -117
      src/test/java/com/inspect/nvr/hik/controller/HikNvrPlaybackHlsControllerTest.java
  9. +0
    -254
      src/test/java/com/inspect/nvr/hik/controller/HikNvrPlaybackUiControllerTest.java
  10. +0
    -152
      src/test/java/com/inspect/nvr/hik/controller/HikNvrUiActionControllerTest.java
  11. +0
    -19
      src/test/java/com/inspect/nvr/hik/domain/HikNvrPlaybackHlsControlRequestTest.java
  12. +0
    -253
      src/test/java/com/inspect/nvr/hik/service/HikFfmpegH264PublisherTest.java
  13. +0
    -124
      src/test/java/com/inspect/nvr/hik/service/HikNvrCameraTargetServiceTest.java
  14. +0
    -119
      src/test/java/com/inspect/nvr/hik/service/HikNvrCredentialServiceTest.java
  15. +0
    -184
      src/test/java/com/inspect/nvr/hik/service/HikNvrDeviceTreeServiceTest.java
  16. +0
    -101
      src/test/java/com/inspect/nvr/hik/service/HikNvrLiveStreamServiceTest.java
  17. +0
    -175
      src/test/java/com/inspect/nvr/hik/service/HikNvrPlaybackClipExporterTest.java
  18. +0
    -281
      src/test/java/com/inspect/nvr/hik/service/HikNvrPlaybackClipServiceTest.java
  19. +0
    -398
      src/test/java/com/inspect/nvr/hik/service/HikNvrPlaybackHlsServiceTest.java
  20. +0
    -323
      src/test/java/com/inspect/nvr/hik/service/HikNvrPlaybackServiceTest.java
  21. +0
    -337
      src/test/java/com/inspect/nvr/hik/service/HikNvrUiActionServiceTest.java
  22. +0
    -18
      src/test/java/com/inspect/nvr/hik/service/HikPlaybackHlsUrlBuilderTest.java
  23. +0
    -82
      src/test/java/com/inspect/nvr/hik/service/HikPlaybackUrlBuilderTest.java
  24. +0
    -314
      src/test/java/com/inspect/nvr/hik/service/HikSdkPlaybackGatewayTest.java
  25. +0
    -176
      src/test/java/com/inspect/nvr/hik/ui/HikNvrPlaybackPageContractTest.java
  26. +0
    -103
      src/test/java/com/inspect/nvr/ivs/IvsCameraSyncServiceTest.java
  27. +0
    -88
      src/test/java/com/inspect/nvr/ivs/IvsOpenApiServiceTest.java
  28. +0
    -77
      src/test/java/com/inspect/nvr/ivs/util/IvsNvrChannelClientTest.java
  29. +0
    -61
      src/test/java/com/inspect/nvr/service/HikLoginServiceTest.java
  30. +0
    -48
      src/test/java/com/inspect/nvr/service/HikLoginSessionTest.java

+ 3
- 0
.gitignore View File

@ -31,6 +31,9 @@ build/
/sdkLog/
/tmp/
# 测试源码仅在本地保留,不提交到仓库
/src/test/
# 本地设计过程文档不纳入版本库
/docs/superpowers/


+ 0
- 112
src/test/java/com/inspect/nvr/controller/IvsServerControllerTest.java View File

@ -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"));
}
}

+ 0
- 87
src/test/java/com/inspect/nvr/dahua/DahuaIvsVendorServiceTest.java View File

@ -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());
}
}

+ 0
- 156
src/test/java/com/inspect/nvr/dahua/DahuaNvrChannelClientTest.java View File

@ -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<IvsNvrChannelClient.Channel> 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));
}
}

+ 0
- 148
src/test/java/com/inspect/nvr/dahua/DahuaNvrPlaybackServiceTest.java View File

@ -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);
}
}

+ 0
- 35
src/test/java/com/inspect/nvr/domain/ivs/IvsSnapshotCommandResponseTest.java View File

@ -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());
}
}

+ 0
- 179
src/test/java/com/inspect/nvr/hik/controller/HikNvrPlaybackControllerTest.java View File

@ -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<StreamingResponseBody> 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);
}
}

+ 0
- 117
src/test/java/com/inspect/nvr/hik/controller/HikNvrPlaybackHlsControllerTest.java View File

@ -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();
}
}

+ 0
- 254
src/test/java/com/inspect/nvr/hik/controller/HikNvrPlaybackUiControllerTest.java View File

@ -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<HikNvrPlaybackRequest> 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<HikNvrPlaybackHlsControlRequest> 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();
}
}

+ 0
- 152
src/test/java/com/inspect/nvr/hik/controller/HikNvrUiActionControllerTest.java View File

@ -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<com.inspect.nvr.hik.domain.HikNvrUiActionRequest> 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);
}
}

+ 0
- 19
src/test/java/com/inspect/nvr/hik/domain/HikNvrPlaybackHlsControlRequestTest.java View File

@ -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));
}
}

+ 0
- 253
src/test/java/com/inspect/nvr/hik/service/HikFfmpegH264PublisherTest.java View File

@ -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<String> command = Collections.emptyList();
private CapturingProcessFactory(Process process) {
this.process = process;
}
@Override
public Process start(List<String> command) {
this.command = new ArrayList<>(command);
return process;
}
private List<String> 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();
}
}
}

+ 0
- 124
src/test/java/com/inspect/nvr/hik/service/HikNvrCameraTargetServiceTest.java View File

@ -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);
}
}

+ 0
- 119
src/test/java/com/inspect/nvr/hik/service/HikNvrCredentialServiceTest.java View File

@ -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;
}
}

+ 0
- 184
src/test/java/com/inspect/nvr/hik/service/HikNvrDeviceTreeServiceTest.java View File

@ -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.<IvsNvr>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.<IvsCamera>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;
}
}

+ 0
- 101
src/test/java/com/inspect/nvr/hik/service/HikNvrLiveStreamServiceTest.java View File

@ -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<IvsVideoRtspVo> 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<String> 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<String> 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"));
}
}

+ 0
- 175
src/test/java/com/inspect/nvr/hik/service/HikNvrPlaybackClipExporterTest.java View File

@ -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<String> 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<HCNetSDK.NET_DVR_PLAYCOND> 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<List<String>> commandCaptor = ArgumentCaptor.forClass(List.class);
verify(processFactory).start(commandCaptor.capture());
List<String> 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();
}
}

+ 0
- 281
src/test/java/com/inspect/nvr/hik/service/HikNvrPlaybackClipServiceTest.java View File

@ -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<HikNvrPlaybackUiSessionRequest> 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()));
}
}

+ 0
- 398
src/test/java/com/inspect/nvr/hik/service/HikNvrPlaybackHlsServiceTest.java View File

@ -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<LocalDateTime> 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<List<String>> commands = new ArrayList<>();
@Override
public Process start(java.util.List<String> command) {
commands.add(new ArrayList<>(command));
return new TestProcess();
}
private String frameRateAt(int commandIndex) {
List<String> 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;
}
}
}

+ 0
- 323
src/test/java/com/inspect/nvr/hik/service/HikNvrPlaybackServiceTest.java View File

@ -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<HCNetSDK.NET_DVR_FILECOND_V40> 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<HCNetSDK.NET_DVR_FILECOND_V40> 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<HCNetSDK.NET_DVR_FILECOND_V40> 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<Integer> 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;
}
}

+ 0
- 337
src/test/java/com/inspect/nvr/hik/service/HikNvrUiActionServiceTest.java View File

@ -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(("<?xml version=\"1.0\" encoding=\"UTF-8\"?><PTZPresets>"
+ "<PTZPreset><id>1</id><presetName>").getBytes(StandardCharsets.UTF_8));
xml.write("正确".getBytes(StandardCharsets.UTF_8));
xml.write(("</presetName></PTZPreset><PTZPreset><id>3</id><presetName>")
.getBytes(StandardCharsets.UTF_8));
xml.write("预置点 3".getBytes(Charset.forName("GBK")));
xml.write("</presetName></PTZPreset></PTZPresets>"
.getBytes(StandardCharsets.UTF_8));
String normalized = ReflectionTestUtils.invokeMethod(
service, "normalizePresetXml", xml.toByteArray());
assertTrue(normalized.contains("<presetName>正确</presetName>"));
assertTrue(normalized.contains("<presetName>预置点 3</presetName>"));
}
@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("<presetName>测试大门</presetName>"));
assertEquals(-1, new String(wireBytes, Charset.forName("GBK"))
.indexOf("<presetName>测试大门</presetName>"));
}
}

+ 0
- 18
src/test/java/com/inspect/nvr/hik/service/HikPlaybackHlsUrlBuilderTest.java View File

@ -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));
}
}

+ 0
- 82
src/test/java/com/inspect/nvr/hik/service/HikPlaybackUrlBuilderTest.java View File

@ -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();
}
}

+ 0
- 314
src/test/java/com/inspect/nvr/hik/service/HikSdkPlaybackGatewayTest.java View File

@ -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<HCNetSDK.NET_DVR_VOD_PARA> 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<HCNetSDK.NET_DVR_VOD_PARA> 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<HikPlaybackFrame> frames = new ArrayList<>();
HikSdkPlaybackHandle handle = (HikSdkPlaybackHandle)
gateway.open(request(), start(), end(), frames::add);
ArgumentCaptor<HCNetSDK.FPlayESCallBack> 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<HikPlaybackFrame> nextSinkFrames = new ArrayList<>();
handle.setFrameSink(nextSinkFrames::add);
assertEquals(1, nextSinkFrames.size());
assertEquals(0, nextSinkFrames.get(0).getPacketType());
}
@Test
public void replaysParameterSetsEmbeddedInAnIFrameWhenSinkChanges() {
List<HikPlaybackFrame> frames = new ArrayList<>();
HikSdkPlaybackHandle handle = (HikSdkPlaybackHandle)
gateway.open(request(), start(), end(), frames::add);
ArgumentCaptor<HCNetSDK.FPlayESCallBack> 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<HikPlaybackFrame> 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<HCNetSDK.FPlayESCallBack> 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<HCNetSDK.FPlayESCallBack> 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;
}
}

+ 0
- 176
src/test/java/com/inspect/nvr/hik/ui/HikNvrPlaybackPageContractTest.java View File

@ -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);
}
}
}

+ 0
- 103
src/test/java/com/inspect/nvr/ivs/IvsCameraSyncServiceTest.java View File

@ -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<IvsCamera> 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<IvsCamera> 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;
}
}

+ 0
- 88
src/test/java/com/inspect/nvr/ivs/IvsOpenApiServiceTest.java View File

@ -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);
}
}

+ 0
- 77
src/test/java/com/inspect/nvr/ivs/util/IvsNvrChannelClientTest.java View File

@ -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<IvsNvrChannelClient.Channel> 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 version=\"1.0\" encoding=\"UTF-8\"?>"
+ "<InputProxyChannel><name>大门入口摄像机</name></InputProxyChannel>";
// 调用实际响应解码方法验证传给 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 = "<name>大门入口摄像机</name>".getBytes(Charset.forName("GBK"));
// 严格解码应抛出异常由同步任务记录具体 NVR而不是继续保存乱码
ReflectionTestUtils.invokeMethod(new IvsNvrChannelClient(), "decode", bytes);
}
}

+ 0
- 61
src/test/java/com/inspect/nvr/service/HikLoginServiceTest.java View File

@ -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;
}
}

+ 0
- 48
src/test/java/com/inspect/nvr/service/HikLoginSessionTest.java View File

@ -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));
}
}

Loading…
Cancel
Save