package com.inspect.nvr.hik.service; import com.inspect.nvr.domain.Infrared.NvrInfo; import com.inspect.nvr.hik.domain.HikNvrPlaybackRequest; import com.inspect.nvr.hik.domain.HikNvrPlaybackResponse; import com.inspect.nvr.hik.exception.HikNvrPlaybackException; import com.inspect.nvr.hikVision.utils.jna.HCNetSDK; import com.inspect.nvr.service.HikLoginService; import com.inspect.nvr.service.HikLoginSession; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.MockitoJUnitRunner; import org.mockito.stubbing.Answer; import org.springframework.test.util.ReflectionTestUtils; import java.time.LocalDateTime; import java.util.Arrays; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class HikNvrPlaybackServiceTest { @Mock private HikLoginService hikLoginService; @Mock private HCNetSDK hcNetSDK; private HikNvrPlaybackService service; @Before public void setUp() { service = new HikNvrPlaybackService(); ReflectionTestUtils.setField(service, "hikLoginService", hikLoginService); ReflectionTestUtils.setField(service, "hcNetSDK", hcNetSDK); ReflectionTestUtils.setField(service, "urlBuilder", new HikPlaybackUrlBuilder()); } @Test public void returnsClippedMergedRangesAndKeepsRealGaps() { prepareSearch(); doAnswer(file("2026-07-13T12:20:00", "2026-07-13T12:40:00")) .doAnswer(file("2026-07-13T12:30:00", "2026-07-13T12:50:00")) .doAnswer(file("2026-07-13T12:55:00", "2026-07-13T13:10:00")) .doReturn(HCNetSDK.NET_DVR_NOMOREFILE) .when(hcNetSDK).NET_DVR_FindNextFile_V40( eq(88), any(HCNetSDK.NET_DVR_FINDDATA_V40.class)); HikNvrPlaybackResponse response = service.search(request()); assertTrue(response.isHasRecord()); assertEquals(2, response.getRecords().size()); assertEquals(Arrays.asList("20260713122000", "20260713125000"), response.getRecords().get(0).getTimeRange()); assertEquals(Arrays.asList("20260713125500", "20260713130000"), response.getRecords().get(1).getTimeRange()); assertTrue(response.getRecords().get(1).getRtspUrl() .contains("starttime=20260713T125500Z&endtime=20260713T130000Z")); verify(hcNetSDK).NET_DVR_FindClose_V30(88); ArgumentCaptor conditionCaptor = ArgumentCaptor.forClass(HCNetSDK.NET_DVR_FILECOND_V40.class); verify(hcNetSDK).NET_DVR_FindFile_V40(eq(7), conditionCaptor.capture()); HCNetSDK.NET_DVR_FILECOND_V40 condition = conditionCaptor.getValue(); assertEquals(33, condition.lChannel); assertEquals(0xff, condition.dwFileType); assertEquals(0xff, condition.dwIsLocked); assertEquals(3, condition.byStreamType); assertEquals(12, condition.struStartTime.dwHour); assertEquals(13, condition.struStopTime.dwHour); } @Test public void keepsSdkChannelThirtyThreeAndBuildsRtspTrackOne() { prepareSearch(); doAnswer(file("2026-07-13T12:00:00", "2026-07-13T13:00:00")) .doReturn(HCNetSDK.NET_DVR_NOMOREFILE) .when(hcNetSDK).NET_DVR_FindNextFile_V40( eq(88), any(HCNetSDK.NET_DVR_FINDDATA_V40.class)); HikNvrPlaybackRequest request = request(); request.setChannel(33); HikNvrPlaybackResponse response = service.search(request); assertTrue(response.getRecords().get(0).getRtspUrl() .contains("/Streaming/tracks/101?starttime=20260713T120000Z" + "&endtime=20260713T130000Z")); ArgumentCaptor conditionCaptor = ArgumentCaptor.forClass(HCNetSDK.NET_DVR_FILECOND_V40.class); verify(hcNetSDK).NET_DVR_FindFile_V40(eq(7), conditionCaptor.capture()); assertEquals(33, conditionCaptor.getValue().lChannel); } @Test public void returnsEmptyResultWhenNvrHasNoRecordings() { prepareSearch(); when(hcNetSDK.NET_DVR_FindNextFile_V40( eq(88), any(HCNetSDK.NET_DVR_FINDDATA_V40.class))) .thenReturn(HCNetSDK.NET_DVR_FILE_NOFIND); HikNvrPlaybackResponse response = service.search(request()); assertFalse(response.isHasRecord()); assertTrue(response.getRecords().isEmpty()); verify(hcNetSDK).NET_DVR_FindClose_V30(88); } @Test public void mergesFilesThatMeetAtExactlyTheSameSecond() { prepareSearch(); doAnswer(file("2026-07-13T12:30:00", "2026-07-13T12:45:00")) .doAnswer(file("2026-07-13T12:45:00", "2026-07-13T13:00:00")) .doReturn(HCNetSDK.NET_DVR_NOMOREFILE) .when(hcNetSDK).NET_DVR_FindNextFile_V40( eq(88), any(HCNetSDK.NET_DVR_FINDDATA_V40.class)); HikNvrPlaybackResponse response = service.search(request()); assertEquals(1, response.getRecords().size()); assertEquals(Arrays.asList("20260713123000", "20260713130000"), response.getRecords().get(0).getTimeRange()); } @Test public void clipsRecordingThatStartsBeforeRequestedRange() { prepareSearch(); doAnswer(file("2026-07-13T11:50:00", "2026-07-13T12:30:00")) .doReturn(HCNetSDK.NET_DVR_NOMOREFILE) .when(hcNetSDK).NET_DVR_FindNextFile_V40( eq(88), any(HCNetSDK.NET_DVR_FINDDATA_V40.class)); HikNvrPlaybackResponse response = service.search(request()); assertEquals(Arrays.asList("20260713120000", "20260713123000"), response.getRecords().get(0).getTimeRange()); } @Test public void mapsSubStreamRequestToSdkSubStreamSearchType() { prepareSearch(); when(hcNetSDK.NET_DVR_FindNextFile_V40( eq(88), any(HCNetSDK.NET_DVR_FINDDATA_V40.class))) .thenReturn(HCNetSDK.NET_DVR_FILE_NOFIND); HikNvrPlaybackRequest request = request(); request.setStreamType(2); service.search(request); ArgumentCaptor conditionCaptor = ArgumentCaptor.forClass(HCNetSDK.NET_DVR_FILECOND_V40.class); verify(hcNetSDK).NET_DVR_FindFile_V40(eq(7), conditionCaptor.capture()); assertEquals(1, conditionCaptor.getValue().byStreamType); } @Test public void retriesFindingStatusBeforeReturningRecord() { prepareSearch(); doReturn(HCNetSDK.NET_DVR_ISFINDING) .doAnswer(file("2026-07-13T12:30:00", "2026-07-13T13:00:00")) .doReturn(HCNetSDK.NET_DVR_NOMOREFILE) .when(hcNetSDK).NET_DVR_FindNextFile_V40( eq(88), any(HCNetSDK.NET_DVR_FINDDATA_V40.class)); HikNvrPlaybackResponse response = service.search(request()); assertTrue(response.isHasRecord()); assertEquals(Arrays.asList("20260713123000", "20260713130000"), response.getRecords().get(0).getTimeRange()); verify(hcNetSDK).NET_DVR_FindClose_V30(88); } @Test public void propagatesSdkErrorAndStillClosesFindHandle() { prepareSearch(); when(hcNetSDK.NET_DVR_FindNextFile_V40( eq(88), any(HCNetSDK.NET_DVR_FINDDATA_V40.class))) .thenReturn(HCNetSDK.NET_DVR_FILE_EXCEPTION); when(hcNetSDK.NET_DVR_GetLastError()).thenReturn(7); try { service.search(request()); fail("Expected HikNvrPlaybackException"); } catch (HikNvrPlaybackException exception) { assertEquals(7, exception.getCode()); } verify(hcNetSDK).NET_DVR_FindClose_V30(88); } @Test public void returnsFindFileErrorWithoutClosingInvalidHandle() { when(hikLoginService.loginSession(any(NvrInfo.class))).thenReturn(loginSession()); when(hcNetSDK.NET_DVR_FindFile_V40( eq(7), any(HCNetSDK.NET_DVR_FILECOND_V40.class))).thenReturn(-1); when(hcNetSDK.NET_DVR_GetLastError()).thenReturn(4); try { service.search(request()); fail("Expected HikNvrPlaybackException"); } catch (HikNvrPlaybackException exception) { assertEquals(4, exception.getCode()); } verify(hcNetSDK, never()).NET_DVR_FindClose_V30(anyInt()); } @Test public void wrapsLoginFailureWithCurrentSdkError() { when(hikLoginService.loginSession(any(NvrInfo.class))) .thenThrow(new RuntimeException("login failed")); when(hcNetSDK.NET_DVR_GetLastError()).thenReturn(7); try { service.search(request()); fail("Expected HikNvrPlaybackException"); } catch (HikNvrPlaybackException exception) { assertEquals(7, exception.getCode()); } verify(hcNetSDK, never()).NET_DVR_FindFile_V40( anyInt(), any(HCNetSDK.NET_DVR_FILECOND_V40.class)); } @Test public void restoresInterruptFlagAndClosesHandleWhenWaitIsInterrupted() { prepareSearch(); when(hcNetSDK.NET_DVR_FindNextFile_V40( eq(88), any(HCNetSDK.NET_DVR_FINDDATA_V40.class))) .thenReturn(HCNetSDK.NET_DVR_ISFINDING); Thread.currentThread().interrupt(); try { service.search(request()); fail("Expected HikNvrPlaybackException"); } catch (HikNvrPlaybackException exception) { assertEquals(-2, exception.getCode()); assertTrue(Thread.currentThread().isInterrupted()); } finally { Thread.interrupted(); } verify(hcNetSDK).NET_DVR_FindClose_V30(88); } @Test public void rejectsEndTimeBeforeStartWithoutLoggingIntoNvr() { HikNvrPlaybackRequest request = request(); request.setEndTime("2026-07-13 11:59:59"); try { service.search(request); fail("Expected HikNvrPlaybackException"); } catch (HikNvrPlaybackException exception) { assertEquals(-1, exception.getCode()); } verify(hikLoginService, never()).loginSession(any(NvrInfo.class)); verify(hcNetSDK, never()).NET_DVR_FindFile_V40( anyInt(), any(HCNetSDK.NET_DVR_FILECOND_V40.class)); } private void prepareSearch() { when(hikLoginService.loginSession(any(NvrInfo.class))).thenReturn(loginSession()); when(hcNetSDK.NET_DVR_FindFile_V40( eq(7), any(HCNetSDK.NET_DVR_FILECOND_V40.class))).thenReturn(88); when(hcNetSDK.NET_DVR_FindClose_V30(88)).thenReturn(true); } private HikLoginSession loginSession() { return new HikLoginSession(7, 0, 1, 8, 33); } private HikNvrPlaybackRequest request() { return HikNvrPlaybackRequest.builder() .nvrIp("192.168.1.100") .sdkPort(8000) .rtspPort(554) .channel(1) .streamType(1) .username("admin") .password("123456") .startTime("2026-07-13 12:00:00") .endTime("2026-07-13 13:00:00") .build(); } private Answer file(String start, String end) { LocalDateTime startTime = LocalDateTime.parse(start); LocalDateTime endTime = LocalDateTime.parse(end); return (InvocationOnMock invocation) -> { HCNetSDK.NET_DVR_FINDDATA_V40 data = invocation.getArgument(1); data.struStartTime = sdkTime(startTime); data.struStopTime = sdkTime(endTime); return HCNetSDK.NET_DVR_FILE_SUCCESS; }; } private HCNetSDK.NET_DVR_TIME sdkTime(LocalDateTime value) { HCNetSDK.NET_DVR_TIME time = new HCNetSDK.NET_DVR_TIME(); time.dwYear = value.getYear(); time.dwMonth = value.getMonthValue(); time.dwDay = value.getDayOfMonth(); time.dwHour = value.getHour(); time.dwMinute = value.getMinute(); time.dwSecond = value.getSecond(); return time; } }