You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

323 lines
12 KiB

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