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