From 9813f2489e30efb8e20450bce491e1261c9bd3f9 Mon Sep 17 00:00:00 2001 From: RM Date: Mon, 5 Oct 2026 14:58:33 +0300 Subject: [PATCH] second tests --- .../DownloadHeadlessControllerTest.java | 443 +++++++++++++++++- 1 file changed, 442 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/pledge/torgi/controller/DownloadHeadlessControllerTest.java b/src/test/java/com/pledge/torgi/controller/DownloadHeadlessControllerTest.java index 0a9b02b..ac7b914 100644 --- a/src/test/java/com/pledge/torgi/controller/DownloadHeadlessControllerTest.java +++ b/src/test/java/com/pledge/torgi/controller/DownloadHeadlessControllerTest.java @@ -1,30 +1,67 @@ package com.pledge.torgi.controller; +import com.pledge.torgi.model.MissedData; import com.pledge.torgi.model.TorgiEvent; import com.pledge.torgi.repositories.MissedDataRepository; import com.pledge.torgi.repositories.TorgiEventRepository; +import com.pledge.torgi.service.DownloadHeadlessService; +import com.pledge.torgi.service.Lot; +import com.pledge.torgi.service.SendEmailService; +import com.pledge.torgi.utils.StartParams; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; +import org.mockito.MockedConstruction; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; -import static org.mockito.ArgumentMatchers.any; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class DownloadHeadlessControllerTest { + private static final String CADASTRAL = "77:01:0001001:1234"; + private static final String EVENT_KEY = "Notice1_1_PUBLISHED_" + CADASTRAL; + @Mock private TorgiEventRepository eventRepository; @Mock private MissedDataRepository missedDataRepository; + @BeforeEach + void setUp() { + lenient().when(eventRepository.findAllByOrderByCreatedAtDesc(any(PageRequest.class))) + .thenReturn(new PageImpl<>(List.of())); + } + @Test void constructor_shouldLoadExistingKeys() { // given @@ -41,4 +78,408 @@ class DownloadHeadlessControllerTest { // then verify(eventRepository).findAllByOrderByCreatedAtDesc(any(PageRequest.class)); } + + @Test + void constructor_shouldLoadAllKeysIntoContentSet() { + // given + TorgiEvent event1 = new TorgiEvent(); + event1.setKey("key-1"); + TorgiEvent event2 = new TorgiEvent(); + event2.setKey("key-2"); + when(eventRepository.findAllByOrderByCreatedAtDesc(any(PageRequest.class))) + .thenReturn(new PageImpl<>(List.of(event1, event2))); + + // when + DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository); + + // then + assertThat(getContent(controller)).containsExactlyInAnyOrder("key-1", "key-2"); + } + + @Test + void process_shouldSaveNewEvents() { + // given + Lot lot = new Lot(); + lot.setStatus("PUBLISHED"); + lot.setNotice("Notice1"); + lot.setLotNumber(1); + lot.setCadastral(CADASTRAL); + lot.setFullInfo("Full info"); + Map downloaded = Map.of(EVENT_KEY, lot); + + try (MockedConstruction ignored = mockConstruction(DownloadHeadlessService.class, + (mock, context) -> when(mock.downloadExcel(anyInt(), anyString())).thenReturn(downloaded))) { + DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository); + + // when + controller.process(); + + // then + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(eventRepository).saveAll(captor.capture()); + List saved = captor.getValue(); + assertThat(saved).hasSize(1); + assertThat(saved.get(0).getKey()).isEqualTo(EVENT_KEY); + assertThat(saved.get(0).getStatus()).isEqualTo("PUBLISHED"); + assertThat(saved.get(0).getNotice()).isEqualTo("Notice1"); + assertThat(saved.get(0).getLotNumber()).isEqualTo(1); + assertThat(saved.get(0).getCadastral()).isEqualTo(CADASTRAL); + assertThat(saved.get(0).getFullInfo()).isEqualTo("Full info"); + assertThat(getContent(controller)).contains(EVENT_KEY); + } + } + + @Test + void process_shouldSkipAlreadyKnownKeys() { + // given + TorgiEvent existing = new TorgiEvent(); + existing.setKey(EVENT_KEY); + when(eventRepository.findAllByOrderByCreatedAtDesc(any(PageRequest.class))) + .thenReturn(new PageImpl<>(List.of(existing))); + Map downloaded = Map.of(EVENT_KEY, new Lot()); + + try (MockedConstruction ignored = mockConstruction(DownloadHeadlessService.class, + (mock, context) -> when(mock.downloadExcel(anyInt(), anyString())).thenReturn(downloaded))) { + DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository); + + // when + controller.process(); + + // then + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(eventRepository).saveAll(captor.capture()); + assertThat(captor.getValue()).isEmpty(); + assertThat(getContent(controller)).containsExactly(EVENT_KEY); + } + } + + @Test + void sendNewContent_shouldCreateDocumentSendArchiveAndCleanUp() throws Exception { + // given + StartParams params = new StartParams(); + params.setFrom("from@example.com"); + params.setToken("token"); + List cadastral = List.of(CADASTRAL); + + try (MockedConstruction serviceConstruction = mockConstruction(DownloadHeadlessService.class); + MockedConstruction emailConstruction = mockConstruction(SendEmailService.class)) { + DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository); + + // when + controller.sendNewContent(params, cadastral, "to@example.com"); + + // then + DownloadHeadlessService service = serviceConstruction.constructed().get(0); + ArgumentCaptor dateCaptor = ArgumentCaptor.forClass(LocalDate.class); + verify(service).createNewDocument(eq(eventRepository), eq(cadastral), dateCaptor.capture()); + LocalDate now = dateCaptor.getValue(); + + SendEmailService emailService = emailConstruction.constructed().get(0); + verify(emailService).sendMail(eq("from@example.com"), eq("to@example.com"), + eq("Новая Выгрузка по торгам от " + now), + eq("Новая Выгрузка по торгам от " + now), + argThat((File file) -> file != null && file.getName().equals(now + ".zip"))); + + assertThat(new File(now + ".zip")).doesNotExist(); + assertThat(new File(now.toString())).doesNotExist(); + } + } + + @Test + void sendArchive_shouldSwallowEmailException() throws Exception { + // given + StartParams params = new StartParams(); + params.setFrom("from@example.com"); + params.setToken("token"); + DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository); + + try (MockedConstruction ignored = mockConstruction(SendEmailService.class, + (mock, context) -> doThrow(new RuntimeException("smtp failure")) + .when(mock).sendMail(anyString(), anyString(), anyString(), anyString(), any(File.class)))) { + + // when / then + assertThatCode(() -> invokeSendArchive(controller, params, LocalDate.now(), "to@example.com")) + .doesNotThrowAnyException(); + SendEmailService emailService = ignored.constructed().get(0); + verify(emailService).sendMail(anyString(), anyString(), anyString(), anyString(), any(File.class)); + } + } + + @Test + void richData_shouldSaveNewEventsForMissedCadastral() { + // given + MissedData missed = new MissedData(); + missed.setCadastral(CADASTRAL); + when(missedDataRepository.findAll(any(PageRequest.class))).thenReturn(new PageImpl<>(List.of(missed))); + when(eventRepository.findAllByFullInfoLikeOrderByCreatedAtDesc("%" + CADASTRAL + "%")).thenReturn(Collections.emptyList()); + + Lot lot = new Lot(); + lot.setStatus("PUBLISHED"); + lot.setNotice("Notice1"); + lot.setLotNumber(1); + lot.setCadastral(CADASTRAL); + Map downloaded = Map.of(EVENT_KEY, lot); + + try (MockedConstruction ignored = mockConstruction(DownloadHeadlessService.class, + (mock, context) -> when(mock.downloadExcel(anyInt(), anyString())).thenReturn(downloaded))) { + DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository); + + // when + controller.richData(); + + // then + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(eventRepository).saveAll(captor.capture()); + assertThat(captor.getValue()).hasSize(1); + assertThat(captor.getValue().get(0).getKey()).isEqualTo(EVENT_KEY); + verify(missedDataRepository).delete(missed); + } + } + + @Test + void richData_shouldSkipKnownEventsForCadastral() { + // given + MissedData missed = new MissedData(); + missed.setCadastral(CADASTRAL); + when(missedDataRepository.findAll(any(PageRequest.class))).thenReturn(new PageImpl<>(List.of(missed))); + + TorgiEvent existing = new TorgiEvent(); + existing.setKey(EVENT_KEY); + when(eventRepository.findAllByFullInfoLikeOrderByCreatedAtDesc("%" + CADASTRAL + "%")).thenReturn(List.of(existing)); + + Map downloaded = Map.of(EVENT_KEY, new Lot()); + + try (MockedConstruction ignored = mockConstruction(DownloadHeadlessService.class, + (mock, context) -> when(mock.downloadExcel(anyInt(), anyString())).thenReturn(downloaded))) { + DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository); + + // when + controller.richData(); + + // then + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(eventRepository).saveAll(captor.capture()); + assertThat(captor.getValue()).isEmpty(); + verify(missedDataRepository).delete(missed); + } + } + + @Test + void richData_shouldDeleteMissedDataWhenDownloadedContentEmpty() { + // given + MissedData missed = new MissedData(); + missed.setCadastral(CADASTRAL); + when(missedDataRepository.findAll(any(PageRequest.class))).thenReturn(new PageImpl<>(List.of(missed))); + + try (MockedConstruction ignored = mockConstruction(DownloadHeadlessService.class, + (mock, context) -> when(mock.downloadExcel(anyInt(), anyString())).thenReturn(Collections.emptyMap()))) { + DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository); + + // when + controller.richData(); + + // then + verify(eventRepository, never()).saveAll(anyList()); + verify(missedDataRepository).delete(missed); + } + } + + @Test + void richData_shouldAbortProcessingOnRepositoryError() { + // given + MissedData missed = new MissedData(); + missed.setCadastral(CADASTRAL); + when(missedDataRepository.findAll(any(PageRequest.class))).thenReturn(new PageImpl<>(List.of(missed))); + when(eventRepository.findAllByFullInfoLikeOrderByCreatedAtDesc(anyString())) + .thenThrow(new RuntimeException("Database error")); + + Map downloaded = Map.of(EVENT_KEY, new Lot()); + + try (MockedConstruction ignored = mockConstruction(DownloadHeadlessService.class, + (mock, context) -> when(mock.downloadExcel(anyInt(), anyString())).thenReturn(downloaded))) { + DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository); + + // when + controller.richData(); + + // then + verify(eventRepository, never()).saveAll(anyList()); + verify(missedDataRepository, never()).delete(any()); + } + } + + @Test + void richData_shouldHandleExceptionGracefully() { + // given + when(missedDataRepository.findAll(any(PageRequest.class))).thenThrow(new RuntimeException("Database error")); + DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository); + + // when / then + assertThatCode(controller::richData).doesNotThrowAnyException(); + } + + @Test + void deleteDirectory_shouldDeleteNestedStructure(@TempDir File tempDir) throws Exception { + // given + File nested = new File(tempDir, "nested"); + File deeper = new File(nested, "deeper"); + assertThat(deeper.mkdirs()).isTrue(); + writeFile(new File(deeper, "file.txt"), "content"); + DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository); + + // when + boolean result = invokeDeleteDirectory(controller, nested); + + // then + assertThat(result).isTrue(); + assertThat(nested).doesNotExist(); + assertThat(tempDir).exists(); + } + + @Test + void zipFile_shouldAddFileEntry(@TempDir File tempDir) throws Exception { + // given + File file = new File(tempDir, "data.txt"); + writeFile(file, "hello"); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + // when + try (ZipOutputStream zipOut = new ZipOutputStream(baos)) { + invokeZipFile(file, "dir/data.txt", zipOut); + } + + // then + try (ZipInputStream zipIn = new ZipInputStream(new ByteArrayInputStream(baos.toByteArray()))) { + ZipEntry entry = zipIn.getNextEntry(); + assertThat(entry).isNotNull(); + assertThat(entry.getName()).isEqualTo("dir/data.txt"); + assertThat(new String(zipIn.readAllBytes(), StandardCharsets.UTF_8)).isEqualTo("hello"); + assertThat(zipIn.getNextEntry()).isNull(); + } + } + + @Test + void zipFile_shouldAddDirectoryEntry(@TempDir File tempDir) throws Exception { + // given + File dir = new File(tempDir, "mydir"); + assertThat(dir.mkdirs()).isTrue(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + // when + try (ZipOutputStream zipOut = new ZipOutputStream(baos)) { + invokeZipFile(dir, "mydir", zipOut); + } + + // then + try (ZipInputStream zipIn = new ZipInputStream(new ByteArrayInputStream(baos.toByteArray()))) { + ZipEntry entry = zipIn.getNextEntry(); + assertThat(entry).isNotNull(); + assertThat(entry.getName()).isEqualTo("mydir/"); + assertThat(entry.isDirectory()).isTrue(); + assertThat(zipIn.getNextEntry()).isNull(); + } + } + + @Test + void createArchive_shouldCreateZipFileForDirectory() throws Exception { + // given + LocalDate now = LocalDate.of(1999, 1, 1); + File sourceDir = new File(now.toString()); + File nested = new File(sourceDir, "nested"); + assertThat(nested.mkdirs()).isTrue(); + writeFile(new File(nested, "data.txt"), "archive-content"); + DownloadHeadlessController controller = new DownloadHeadlessController(eventRepository, missedDataRepository); + + try { + // when + invokeCreateArchive(controller, now); + + // then + File zip = new File(now + ".zip"); + assertThat(zip).exists(); + assertThat(readEntryNames(zip)).containsExactlyInAnyOrder( + "1999-01-01/", "1999-01-01/nested/", "1999-01-01/nested/data.txt"); + } finally { + new File(now + ".zip").delete(); + deleteRecursively(sourceDir); + } + } + + // Helpers + + @SuppressWarnings("unchecked") + private Set getContent(DownloadHeadlessController controller) { + try { + Field field = DownloadHeadlessController.class.getDeclaredField("content"); + field.setAccessible(true); + return (Set) field.get(controller); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void invokeCreateArchive(DownloadHeadlessController controller, LocalDate now) { + try { + Method method = DownloadHeadlessController.class.getDeclaredMethod("createArchive", LocalDate.class); + method.setAccessible(true); + method.invoke(controller, now); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private boolean invokeDeleteDirectory(DownloadHeadlessController controller, File directory) { + try { + Method method = DownloadHeadlessController.class.getDeclaredMethod("deleteDirectory", File.class); + method.setAccessible(true); + return (boolean) method.invoke(controller, directory); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void invokeSendArchive(DownloadHeadlessController controller, StartParams params, LocalDate now, String to) { + try { + Method method = DownloadHeadlessController.class.getDeclaredMethod("sendArchive", StartParams.class, LocalDate.class, String.class); + method.setAccessible(true); + method.invoke(controller, params, now, to); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static void invokeZipFile(File fileToZip, String fileName, ZipOutputStream zipOut) { + try { + Method method = DownloadHeadlessController.class.getDeclaredMethod("zipFile", File.class, String.class, ZipOutputStream.class); + method.setAccessible(true); + method.invoke(null, fileToZip, fileName, zipOut); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void writeFile(File file, String content) throws IOException { + Files.write(file.toPath(), content.getBytes(StandardCharsets.UTF_8)); + } + + private List readEntryNames(File zipFile) throws IOException { + List names = new ArrayList<>(); + try (ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFile))) { + ZipEntry entry; + while ((entry = zipIn.getNextEntry()) != null) { + names.add(entry.getName()); + } + } + return names; + } + + private void deleteRecursively(File file) { + File[] children = file.listFiles(); + if (children != null) { + for (File child : children) { + deleteRecursively(child); + } + } + file.delete(); + } }