ms-incoming-transfer-handler
package az.kapitalbank.atlas.incoming.transfer.handler.service;
import static az.kapitalbank.atlas.incoming.transfer.handler.config.properties.ApplicationConstants.FILE_READ_DELAY_MS;
import az.kapitalbank.atlas.incoming.transfer.handler.config.properties.FileExtensionProperties;
import az.kapitalbank.atlas.incoming.transfer.handler.config.properties.FolderProperties;
import az.kapitalbank.atlas.incoming.transfer.handler.config.properties.ProcessingBehaviorProperties;
import az.kapitalbank.atlas.incoming.transfer.handler.util.SwiftMessageUtil;
import com.prowidesoftware.swift.model.SwiftMessage;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.stream.Stream;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class FileWatcherHandler {
private final MT103Service mt103Service;
private final FolderProperties properties;
private final TransferProcessorService processingService;
private final FileExtensionProperties fileExtensionProperties;
private final ProcessingBehaviorProperties behaviorProperties;
@Scheduled(cron = "${mt103.retry.failedConsume}")
public void consumeFilesEveryHour() {
Path folderPath = Paths.get(properties.getFolder());
processExistingFiles(folderPath);
}
public void watchDirectory() {
Path folderPath = Paths.get(properties.getFolder());
try (WatchService watchService = FileSystems.getDefault().newWatchService()) {
processExistingFiles(folderPath);
folderPath.register(watchService, StandardWatchEventKinds.ENTRY_CREATE);
log.info("Started watching directory: {}", folderPath);
while (true) {
WatchKey key;
try {
key = watchService.take();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("Watch service interrupted; exiting properly.", e);
return;
}
for (WatchEvent<?> event : key.pollEvents()) {
if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
Path newFilePath = folderPath.resolve((Path) event.context());
if (hasAllowedExtension(newFilePath)) {
log.info("New file detected: {}", newFilePath);
processMT103File(newFilePath);
}
}
}
boolean valid = key.reset();
if (!valid) {
log.warn("WatchKey is no longer valid. Stopping directory watch.");
break;
}
}
} catch (IOException e) {
log.error("I/O error in watchDirectory. Stopping watch loop.", e);
}
}
public void processExistingFiles(Path folderPath) {
try (Stream<Path> files = Files.list(folderPath)) {
files.filter(this::hasAllowedExtension)
.forEach(this::processMT103File);
} catch (IOException e) {
log.error("Error reading existing files in folder {}: {}", folderPath, e.getMessage(), e);
}
}
public void processMT103File(Path filePath) {
try {
if (!Files.exists(filePath)) {
log.warn("File {} no longer exists. Skipping...", filePath);
return;
}
Thread.sleep(FILE_READ_DELAY_MS);
String content = Files.readString(filePath);
log.info("Processing file: {}", filePath);
SwiftMessage swiftMessage = SwiftMessageUtil.parseSwiftMessage(content);
if (!SwiftMessageUtil.isMt103(swiftMessage)) {
log.warn("File {} is NOT an MT-103 (detected MT-{})", filePath, swiftMessage.getType());
handleUnsuccessFile(filePath);
return;
}
String gpiRef = SwiftMessageUtil.extractGpiRef(swiftMessage);
log.info("Started processReadStepAndSaveMT103 file: {}, with gpiRef: {}", filePath, gpiRef);
processingService.processReadStepAndSaveMT103(gpiRef, content);
log.info("Finished processing processReadStepAndSaveMT103 file: {}, with gpiRef: {}", filePath, gpiRef);
if (behaviorProperties.isDeleteAfterProcessing()) {
deleteFile(gpiRef, filePath);
} else {
moveFileToSuccessFolder(filePath);
}
String block4Content = SwiftMessageUtil.extractBlock4(swiftMessage);
log.info("Started verifyTransfer file: {}, with gpiRef: {}", filePath, gpiRef);
processingService.verifyTransfer(content, gpiRef, block4Content);
log.info("Finished verifyTransfer file: {}, with gpiRef: {}", filePath, gpiRef);
} catch (IOException e) {
log.error("Error reading file {}: {}", filePath, e.getMessage(), e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("Interrupted while processing file: {}", filePath, e);
} catch (Exception e) {
log.error("Unexpected error processing file {}: {}", filePath, e.getMessage(), e);
}
}
private void deleteFile(String gpiRef, Path filePath) {
try {
if (mt103Service.isPresent(gpiRef)) {
Files.deleteIfExists(filePath);
log.info("Deleted processed file: {}", filePath);
}
} catch (IOException e) {
log.error("Failed to delete file: {}", filePath, e);
}
}
private void handleUnsuccessFile(Path filePath) {
try {
if (behaviorProperties.isDeleteAfterProcessing()) {
Files.deleteIfExists(filePath);
log.info("Deleted non-MT-103 file: {}", filePath);
} else {
moveFileToUnsuccessFolder(filePath);
}
} catch (IOException ioe) {
log.error("Failed to handle non-MT-103 file {}", filePath, ioe);
}
}
private void moveFileToSubFolder(Path filePath, String subFolder, String descriptor) {
try {
Path targetDir = Paths.get(properties.getFolder(), subFolder);
Files.createDirectories(targetDir);
Path targetPath = targetDir.resolve(filePath.getFileName());
Files.move(filePath, targetPath);
log.info("Moved {} file {} to {}", descriptor, filePath, targetPath);
} catch (IOException ioe) {
log.error("Failed to move {} file {}", descriptor, filePath, ioe);
}
}
private void moveFileToSuccessFolder(Path filePath) {
moveFileToSubFolder(filePath, "success", "MT-103");
}
private void moveFileToUnsuccessFolder(Path filePath) {
moveFileToSubFolder(filePath, "unsuccess", "non-MT-103");
}
private boolean hasAllowedExtension(Path filePath) {
return fileExtensionProperties.getFileExtensions().stream()
.anyMatch(filePath.toString()::endsWith);
}
}
package az.kapitalbank.atlas.incoming.transfer.handler.service;
import static az.kapitalbank.atlas.incoming.transfer.handler.domain.ProcessStep.VERIFY_PERSON;
import static az.kapitalbank.atlas.incoming.transfer.handler.domain.ProcessStep.VERIFY_TRANSFER;
import static az.kapitalbank.atlas.incoming.transfer.handler.util.SwiftUtil.isRetryableException;
import az.kapitalbank.atlas.incoming.transfer.handler.client.kyc.model.response.VerifyPersonStatusResponse;
import az.kapitalbank.atlas.incoming.transfer.handler.client.kyc.model.response.VerifyTransferResponse;
import az.kapitalbank.atlas.incoming.transfer.handler.config.properties.ZeusCallProperties;
import az.kapitalbank.atlas.incoming.transfer.handler.domain.KycStatusType;
import az.kapitalbank.atlas.incoming.transfer.handler.domain.MT103IncomingTransfer;
import az.kapitalbank.atlas.incoming.transfer.handler.domain.ProcessStatus;
import az.kapitalbank.atlas.incoming.transfer.handler.domain.ProcessStep;
import az.kapitalbank.atlas.lib.common.error.ServiceException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class TransferProcessorService {
private final PaymentService paymentService;
private final NiceActimizerService niceActimizerService;
private final StepLoggerService stepLoggerService;
private final MT103Service mt103Service;
private final ZeusCallProperties zeusCallProperties;
public void resumeFlow(String content, String gpiRef, String block4Content, ProcessStep processStep) {
switch (processStep) {
case VERIFY_TRANSFER -> verifyTransfer(content, gpiRef, block4Content);
case VERIFY_PERSON -> verifyPersonStatus(gpiRef, content);
case SAVE_PAYMENT -> callZeus(gpiRef, content);
default -> log.error("Unexpected failed step: {}", processStep);
}
}
public void processReadStepAndSaveMT103(String gpiRef, String content) {
MT103IncomingTransfer mt103IncomingTransfer = mt103Service.save(gpiRef, content);
stepLoggerService.logSuccess(mt103IncomingTransfer.getId(), ProcessStep.READ_FILE);
}
public void verifyTransfer(String content, String gpiRef, String block4Content) {
try {
VerifyTransferResponse response = niceActimizerService.verifyTransfer(gpiRef, block4Content);
Boolean hasHits = response.getHasHits();
if (hasHits) {
verifyPersonStatus(gpiRef, content);
} else {
callZeus(gpiRef, content);
}
} catch (ServiceException e) {
if (isRetryableException(e)) {
stepLoggerService.logFail(gpiRef, VERIFY_TRANSFER, e.getMessage());
return;
}
stepLoggerService.logFail(gpiRef, VERIFY_TRANSFER,
ProcessStatus.BAD_REQUEST, e.getMessage());
} catch (Exception e) {
log.error("Unexpected exception while verifying transfer. gpiRef={}", gpiRef, e);
stepLoggerService.logFail(gpiRef, VERIFY_TRANSFER, e.getMessage());
}
}
private void verifyPersonStatus(String gpiRef, String content) {
try {
VerifyPersonStatusResponse response = niceActimizerService.getVerifyPersonStatus(gpiRef);
KycStatusType kycStatusType = KycStatusType.valueOf(response.getStatus());
switch (kycStatusType) {
case BLOCK -> stepLoggerService.logBlock(gpiRef, VERIFY_PERSON);
case NOALERT, RELEASE -> callZeus(gpiRef, content);
case PENDING -> stepLoggerService.logPending(gpiRef, VERIFY_PERSON);
default -> throw new IllegalStateException("Unexpected KycStatusType: " + kycStatusType);
}
} catch (Exception e) {
log.error("Error during verifyPerson for gpiRef={}", gpiRef, e);
stepLoggerService.logFail(gpiRef, VERIFY_PERSON, e.getMessage());
}
}
private void callZeus(String gpiRef, String content) {
if (!zeusCallProperties.isCallZeusEnabled()) {
log.info("callZeus disabled by config, skipping for gpiRef={}", gpiRef);
stepLoggerService.logSuccess(gpiRef, ProcessStep.SAVE_PAYMENT);
return;
}
try {
paymentService.saveSwiftPayment(content);
stepLoggerService.logSuccess(gpiRef, ProcessStep.SAVE_PAYMENT);
} catch (Exception e) {
log.error("Error during callZeus for gpiRef={}", gpiRef, e);
stepLoggerService.logFail(gpiRef, ProcessStep.SAVE_PAYMENT, e.getMessage());
}
}
}
package az.kapitalbank.atlas.incoming.transfer.handler.service;
import static az.kapitalbank.atlas.incoming.transfer.handler.config.properties.ApplicationConstants.FILE_READ_DELAY_MS;
import static az.kapitalbank.atlas.incoming.transfer.handler.domain.ProcessStep.READ_FILE;
import az.kapitalbank.atlas.incoming.transfer.handler.config.properties.FileExtensionProperties;
import az.kapitalbank.atlas.incoming.transfer.handler.config.properties.FolderProperties;
import az.kapitalbank.atlas.incoming.transfer.handler.config.properties.ProcessingBehaviorProperties;
import az.kapitalbank.atlas.incoming.transfer.handler.util.SwiftMessageUtil;
import com.prowidesoftware.swift.model.SwiftMessage;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.stream.Stream;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class FileWatcherHandler {
private final MT103Service mt103Service;
private final FolderProperties properties;
private final TransferProcessorService processingService;
private final FileExtensionProperties fileExtensionProperties;
private final ProcessingBehaviorProperties behaviorProperties;
private final StepLoggerService stepLoggerService;
@Scheduled(cron = "${mt103.retry.failedConsume}")
public void consumeFilesEveryHour() {
processExistingFiles(Paths.get(properties.getFolder()));
}
public void watchDirectory() {
Path folder = Paths.get(properties.getFolder());
try (WatchService ws = FileSystems.getDefault().newWatchService()) {
processExistingFiles(folder);
folder.register(ws, StandardWatchEventKinds.ENTRY_CREATE);
log.info("Started watching directory: {}", folder);
while (true) {
WatchKey key = ws.take();
for (WatchEvent<?> ev : key.pollEvents()) {
if (ev.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
Path p = folder.resolve((Path) ev.context());
if (hasAllowedExtension(p)) {
log.info("New file detected: {}", p);
processMT103File(p);
}
}
}
if (!key.reset()) {
log.warn("WatchKey is no longer valid. Stopping directory watch.");
break;
}
}
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
log.warn("Watch service interrupted; exiting.", ie);
} catch (IOException ioe) {
log.error("I/O error in watchDirectory.", ioe);
}
}
public void processExistingFiles(Path folder) {
try (Stream<Path> files = Files.list(folder)) {
files.filter(this::hasAllowedExtension).forEach(this::processMT103File);
} catch (IOException e) {
log.error("Error reading folder {}: {}", folder, e.getMessage(), e);
}
}
public void processMT103File(Path filePath) {
String gpiRef = null;
try {
if (!Files.exists(filePath)) {
log.warn("File {} no longer exists. Skipping...", filePath);
return;
}
Thread.sleep(FILE_READ_DELAY_MS);
String content = Files.readString(filePath);
log.info("Processing file: {}", filePath);
SwiftMessage swift = SwiftMessageUtil.parseSwiftMessage(content);
if (!SwiftMessageUtil.isMt103(swift)) {
log.warn("File {} is NOT an MT-103 (detected MT-{})", filePath, swift.getType());
handleUnsuccessFile(filePath);
return;
}
gpiRef = SwiftMessageUtil.extractGpiRef(swift);
log.info("Started processReadStepAndSaveMT103 file: {}, with gpiRef: {}", filePath, gpiRef);
processingService.processReadStepAndSaveMT103(gpiRef, content);
log.info("Finished processing processReadStepAndSaveMT103 file: {}, with gpiRef: {}", filePath, gpiRef);
if (behaviorProperties.isDeleteAfterProcessing()) {
deleteFile(gpiRef, filePath);
} else {
moveFileToSuccessFolder(filePath);
}
String block4Content = SwiftMessageUtil.extractBlock4(swift);
log.info("Started verifyTransfer file: {}, with gpiRef: {}", filePath, gpiRef);
processingService.verifyTransfer(content, gpiRef, block4Content);
log.info("Finished verifyTransfer file: {}, with gpiRef: {}", filePath, gpiRef);
} catch (IOException e) {
log.error("Error reading file {}: {}", filePath, e.getMessage(), e);
handleFailedRead(filePath);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("Interrupted while processing file: {}", filePath, e);
handleFailedRead(filePath);
} catch (Exception e) {
log.error("Unexpected error processing file {}: {}", filePath, e.getMessage(), e);
if (gpiRef != null) {
stepLoggerService.logFail(gpiRef, READ_FILE, e.getMessage());
}
handleFailedRead(filePath);
}
}
private void handleFailedRead(Path path) {
if (path == null) {
return;
}
if (behaviorProperties.isDeleteAfterProcessing()) {
try {
Files.deleteIfExists(path);
log.info("Deleted FAILED file: {}", path);
} catch (IOException ex) {
log.error("Can't delete FAILED file {}", path, ex);
}
} else {
moveFileToUnsuccessFolder(path);
}
}
private void deleteFile(String gpiRef, Path path) {
try {
if (mt103Service.isPresent(gpiRef)) {
Files.deleteIfExists(path);
log.info("Deleted processed file: {}", path);
}
} catch (IOException ioe) {
log.error("Failed to delete file: {}", path, ioe);
}
}
private void handleUnsuccessFile(Path path) {
if (behaviorProperties.isDeleteAfterProcessing()) {
handleFailedRead(path);
} else {
moveFileToUnsuccessFolder(path);
}
}
private void moveFileToSuccessFolder(Path p) {
moveFileToSubFolder(p, "success", "MT-103");
}
private void moveFileToUnsuccessFolder(Path p) {
moveFileToSubFolder(p, "unsuccess", "non-MT-103");
}
private void moveFileToSubFolder(Path p, String sub, String label) {
try {
Path dstDir = Paths.get(properties.getFolder(), sub);
Files.createDirectories(dstDir);
Path dst = dstDir.resolve(p.getFileName());
Files.move(p, dst);
log.info("Moved {} file {} to {}", label, p, dst);
} catch (IOException ioe) {
log.error("Failed to move {} file {}", label, p, ioe);
}
}
private boolean hasAllowedExtension(Path p) {
String name = p.toString();
return !name.endsWith(".processing")
&& fileExtensionProperties.getFileExtensions().stream().anyMatch(name::endsWith);
}
}
package az.kapital.atlas.incoming.transfer.handler.service;
import static az.kapital.atlas.incoming.transfer.handler.properties.TestConstants.BLOCK4;
import static az.kapital.atlas.incoming.transfer.handler.properties.TestConstants.GPI_REF;
import static az.kapital.atlas.incoming.transfer.handler.properties.TestConstants.MT103CONTENT;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import az.kapitalbank.atlas.incoming.transfer.handler.config.properties.FileExtensionProperties;
import az.kapitalbank.atlas.incoming.transfer.handler.config.properties.FolderProperties;
import az.kapitalbank.atlas.incoming.transfer.handler.config.properties.ProcessingBehaviorProperties;
import az.kapitalbank.atlas.incoming.transfer.handler.service.FileWatcherHandler;
import az.kapitalbank.atlas.incoming.transfer.handler.service.MT103Service;
import az.kapitalbank.atlas.incoming.transfer.handler.service.StepLoggerService;
import az.kapitalbank.atlas.incoming.transfer.handler.service.TransferProcessorService;
import az.kapitalbank.atlas.incoming.transfer.handler.util.SwiftMessageUtil;
import com.prowidesoftware.swift.model.SwiftMessage;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class FileWatcherHandlerTest {
@Mock
MT103Service mt103Service;
@Mock
FolderProperties folderProps;
@Mock
FileExtensionProperties extProps;
@Mock
ProcessingBehaviorProperties behavProps;
@Mock
TransferProcessorService processor;
@Mock
StepLoggerService stepLoggerService;
@InjectMocks
FileWatcherHandler handler;
@TempDir
Path tmp;
private MockedStatic<SwiftMessageUtil> mockSwift(boolean isMt103) {
MockedStatic<SwiftMessageUtil> util = mockStatic(SwiftMessageUtil.class);
SwiftMessage sm = mock(SwiftMessage.class);
util.when(() -> SwiftMessageUtil.parseSwiftMessage(any())).thenReturn(sm);
util.when(() -> SwiftMessageUtil.isMt103(sm)).thenReturn(isMt103);
util.when(() -> SwiftMessageUtil.extractGpiRef(sm)).thenReturn(GPI_REF);
util.when(() -> SwiftMessageUtil.extractBlock4(sm)).thenReturn(BLOCK4);
return util;
}
@BeforeEach
void init() {
when(folderProps.getFolder()).thenReturn(tmp.toString());
when(extProps.getFileExtensions()).thenReturn(List.of(".txt"));
}
@Test
void mt103_saved_in_success_folder_when_deleteFalse() throws Exception {
Path file = Files.writeString(tmp.resolve("mt.txt"), MT103CONTENT);
Path success = tmp.resolve("success");
try (MockedStatic<SwiftMessageUtil> ignored = mockSwift(true)) {
when(behavProps.isDeleteAfterProcessing()).thenReturn(false);
handler.processMT103File(file);
assertTrue(Files.exists(success.resolve(file.getFileName())));
}
}
@Test
void mt103_deleted_when_deleteTrue_and_presentInDb() throws Exception {
Path file = Files.writeString(tmp.resolve("mt.txt"), MT103CONTENT);
try (MockedStatic<SwiftMessageUtil> ignored = mockSwift(true)) {
when(behavProps.isDeleteAfterProcessing()).thenReturn(true);
when(mt103Service.isPresent(GPI_REF)).thenReturn(true);
handler.processMT103File(file);
assertFalse(Files.exists(file));
}
}
@Test
void mt103_kept_when_deleteTrue_but_notInDb() throws Exception {
Path file = Files.writeString(tmp.resolve("mt.txt"), MT103CONTENT);
try (MockedStatic<SwiftMessageUtil> ignored = mockSwift(true)) {
when(behavProps.isDeleteAfterProcessing()).thenReturn(true);
when(mt103Service.isPresent(GPI_REF)).thenReturn(false);
handler.processMT103File(file);
assertTrue(Files.exists(file));
}
}
@Test
void nonMt103_moved_to_unsuccess_when_deleteFalse() throws Exception {
Path file = Files.writeString(tmp.resolve("bad.txt"), MT103CONTENT);
Path unsuccess = tmp.resolve("unsuccess");
try (MockedStatic<SwiftMessageUtil> ignored = mockSwift(false)) {
when(behavProps.isDeleteAfterProcessing()).thenReturn(false);
handler.processMT103File(file);
assertTrue(Files.exists(unsuccess.resolve(file.getFileName())));
}
}
@Test
void nonMt103_deleted_when_deleteTrue() throws Exception {
Path file = Files.writeString(tmp.resolve("bad.txt"), MT103CONTENT);
try (MockedStatic<SwiftMessageUtil> ignored = mockSwift(false)) {
when(behavProps.isDeleteAfterProcessing()).thenReturn(true);
handler.processMT103File(file);
assertFalse(Files.exists(file));
}
}
@Test
void ioException_while_moving_to_success_is_swallowed() throws Exception {
Path file = Files.writeString(tmp.resolve("mt.txt"), MT103CONTENT);
try (MockedStatic<SwiftMessageUtil> ignored1 = mockSwift(true);
MockedStatic<Files> ignored2 = mockStatic(Files.class, CALLS_REAL_METHODS)) {
when(behavProps.isDeleteAfterProcessing()).thenReturn(false);
ignored2.when(() -> Files.move(any(), any())).thenThrow(new IOException("boom"));
assertDoesNotThrow(() -> handler.processMT103File(file));
}
}
@Test
void ioException_while_moving_to_unsuccess_is_swallowed() throws Exception {
Path file = Files.writeString(tmp.resolve("bad.txt"), MT103CONTENT);
try (MockedStatic<SwiftMessageUtil> ignored1 = mockSwift(false);
MockedStatic<Files> ignored2 = mockStatic(Files.class, CALLS_REAL_METHODS)) {
when(behavProps.isDeleteAfterProcessing()).thenReturn(false);
ignored2.when(() -> Files.move(any(), any())).thenThrow(new IOException("boom"));
assertDoesNotThrow(() -> handler.processMT103File(file));
}
}
@Test
void reading_a_directory_instead_of_file_is_logged_not_thrown() throws Exception {
Path dir = Files.createDirectory(tmp.resolve("badDir"));
assertDoesNotThrow(() -> handler.processMT103File(dir));
}
@Test
void listing_missing_directory_is_logged_not_thrown() {
Path missing = tmp.resolve("does_not_exist");
assertDoesNotThrow(() -> handler.processExistingFiles(missing));
}
@Test
void consumeFilesEveryHour_delegates_to_processExistingFiles() {
FileWatcherHandler spy = spy(handler);
doNothing().when(spy).processExistingFiles(any());
spy.consumeFilesEveryHour();
verify(spy).processExistingFiles(Path.of(tmp.toString()));
}
@Test
void watchDirectory_handles_single_create_event() throws Exception {
WatchService watchService = mock(WatchService.class);
WatchKey watchKey = mock(WatchKey.class);
Path folderMock = mock(Path.class);
Path newFileName = mock(Path.class);
when(folderProps.getFolder()).thenReturn("dummy");
when(newFileName.toString()).thenReturn("incoming.txt");
when(newFileName.getFileName()).thenReturn(newFileName);
when(folderMock.resolve(any(Path.class))).thenReturn(newFileName);
try (MockedStatic<FileSystems> fsMock = mockStatic(FileSystems.class);
MockedStatic<Paths> pMock = mockStatic(Paths.class)) {
FileSystem fs = mock(FileSystem.class);
fsMock.when(FileSystems::getDefault).thenReturn(fs);
when(fs.newWatchService()).thenReturn(watchService);
pMock.when(() -> Paths.get("dummy")).thenReturn(folderMock);
doReturn(watchKey).when(folderMock)
.register(eq(watchService), eq(StandardWatchEventKinds.ENTRY_CREATE));
@SuppressWarnings("unchecked")
WatchEvent<Path> event = mock(WatchEvent.class);
when(event.kind()).thenReturn(StandardWatchEventKinds.ENTRY_CREATE);
when(event.context()).thenReturn(newFileName);
when(watchKey.pollEvents()).thenReturn(List.of(event));
when(watchService.take()).thenReturn(watchKey);
when(watchKey.reset()).thenReturn(false);
FileWatcherHandler spy = spy(handler);
doNothing().when(spy).processExistingFiles(any());
doNothing().when(spy).processMT103File(newFileName);
spy.watchDirectory();
verify(spy).processMT103File(newFileName);
}
}
@Test
@DisplayName("hasAllowedExtension(true) для валидного расширения")
void hasAllowedExtension_true_for_allowed() throws Exception {
Path p = Path.of("invoice.txt");
boolean res = invokeHasAllowedExtension(p);
assertTrue(res);
}
@Test
@DisplayName("hasAllowedExtension(false) для *.processing")
void hasAllowedExtension_false_for_processing() throws Exception {
Path p = Path.of("invoice.txt.processing");
boolean res = invokeHasAllowedExtension(p);
assertFalse(res);
}
@Test
void handleFailedRead_null_is_noop() throws Exception {
Method m = FileWatcherHandler.class
.getDeclaredMethod("handleFailedRead", Path.class);
m.setAccessible(true);
assertDoesNotThrow(() -> m.invoke(handler, new Object[] {null}));
}
@Test
void ioException_during_read_logsFail_with_nullGpi() throws Exception {
Path file = Files.writeString(tmp.resolve("boom.txt"), "ignored");
try (MockedStatic<SwiftMessageUtil> _swift =
mockStatic(SwiftMessageUtil.class);
MockedStatic<Files> filesStub =
mockStatic(Files.class, CALLS_REAL_METHODS)) {
filesStub.when(() -> Files.readString(file))
.thenThrow(new IOException("disk error"));
when(behavProps.isDeleteAfterProcessing()).thenReturn(false);
assertDoesNotThrow(() -> handler.processMT103File(file));
verifyNoInteractions(stepLoggerService);
}
}
@Test
void missing_file_is_skipped_without_side_effects() {
Path ghost = tmp.resolve("ghost.txt");
assertDoesNotThrow(() -> handler.processMT103File(ghost));
verifyNoInteractions(processor, mt103Service, stepLoggerService);
}
@Test
void interrupted_sleep_moves_file_to_unsuccess_when_deleteFalse() throws Exception {
Path f = Files.writeString(tmp.resolve("int.txt"), "any");
when(behavProps.isDeleteAfterProcessing()).thenReturn(false);
Thread.currentThread().interrupt();
try {
handler.processMT103File(f);
} finally {
Thread.interrupted();
}
assertTrue(Files.exists(tmp.resolve("unsuccess").resolve(f.getFileName())));
}
@Test
void ioException_during_read_and_deleteTrue_removes_file() throws Exception {
Path f = Files.writeString(tmp.resolve("io.txt"), "ignored");
try (MockedStatic<Files> fs = mockStatic(Files.class, CALLS_REAL_METHODS)) {
fs.when(() -> Files.readString(f)).thenThrow(new IOException("disk"));
when(behavProps.isDeleteAfterProcessing()).thenReturn(true);
assertDoesNotThrow(() -> handler.processMT103File(f));
assertFalse(Files.exists(f));
}
}
@Test
void deleteFile_swallows_IOException() throws Exception {
Path f = Files.writeString(tmp.resolve("keep.txt"), MT103CONTENT);
try (MockedStatic<SwiftMessageUtil> __ = mockSwift(true);
MockedStatic<Files> fs = mockStatic(Files.class, CALLS_REAL_METHODS)) {
when(behavProps.isDeleteAfterProcessing()).thenReturn(true);
when(mt103Service.isPresent(GPI_REF)).thenReturn(true);
fs.when(() -> Files.deleteIfExists(f)).thenThrow(new IOException("perm"));
assertDoesNotThrow(() -> handler.processMT103File(f));
}
}
@Test
void watchDirectory_handles_InterruptedException_cleanly() throws Exception {
WatchService ws = mock(WatchService.class);
when(ws.take()).thenThrow(new InterruptedException("stop"));
try (MockedStatic<FileSystems> fsMock = mockStatic(FileSystems.class);
MockedStatic<Paths> pathMock = mockStatic(Paths.class)) {
Path folder = mock(Path.class);
WatchKey dummyKey = mock(WatchKey.class);
when(folder.register(eq(ws), eq(StandardWatchEventKinds.ENTRY_CREATE)))
.thenReturn(dummyKey);
FileSystem fs = mock(FileSystem.class);
when(fs.newWatchService()).thenReturn(ws);
fsMock.when(FileSystems::getDefault).thenReturn(fs);
pathMock.when(() -> Paths.get(tmp.toString())).thenReturn(folder);
when(folderProps.getFolder()).thenReturn(tmp.toString());
FileWatcherHandler spy = spy(handler);
doNothing().when(spy).processExistingFiles(any());
assertDoesNotThrow(spy::watchDirectory);
}
}
@Test
void watchDirectory_handles_newWatchService_IOException() {
try (MockedStatic<FileSystems> fsMock = mockStatic(FileSystems.class);
MockedStatic<Paths> pathMock = mockStatic(Paths.class)) {
FileSystem fs = mock(FileSystem.class);
when(fs.newWatchService()).thenThrow(new IOException("nope"));
fsMock.when(FileSystems::getDefault).thenReturn(fs);
pathMock.when(() -> Paths.get("dummy")).thenReturn(mock(Path.class));
when(folderProps.getFolder()).thenReturn("dummy");
FileWatcherHandler spy = spy(handler);
doNothing().when(spy).processExistingFiles(any());
assertDoesNotThrow(spy::watchDirectory);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private boolean invokeHasAllowedExtension(Path p) throws Exception {
Method m = FileWatcherHandler.class
.getDeclaredMethod("hasAllowedExtension", Path.class);
m.setAccessible(true);
return (boolean) m.invoke(handler, p);
}
}
// @Mock
// MT103Service mt103Service;
// @Mock
// FolderProperties folderProps;
// @Mock
// FileExtensionProperties extProps;
// @Mock
// ProcessingBehaviorProperties behavProps;
// @Mock
// TransferProcessorService processor;
// @Mock
// StepLoggerService stepLoggerService;
//
// @InjectMocks
// FileWatcherHandler handler;
//
// @TempDir
// Path tmp;
//
// private MockedStatic<SwiftMessageUtil> mockSwift(boolean isMt103) {
// MockedStatic<SwiftMessageUtil> util = mockStatic(SwiftMessageUtil.class);
// SwiftMessage sm = mock(SwiftMessage.class);
// util.when(() -> SwiftMessageUtil.parseSwiftMessage(any())).thenReturn(sm);
// util.when(() -> SwiftMessageUtil.isMt103(sm)).thenReturn(isMt103);
// util.when(() -> SwiftMessageUtil.extractGpiRef(sm)).thenReturn(GPI_REF);
// util.when(() -> SwiftMessageUtil.extractBlock4(sm)).thenReturn(BLOCK4);
// return util;
// }
//
// @BeforeEach
// void init() {
// when(folderProps.getFolder()).thenReturn(tmp.toString());
// when(extProps.getFileExtensions()).thenReturn(List.of(".txt"));
// }
//
// @Test
// void mt103_saved_in_success_folder_when_deleteFalse() throws Exception {
// Path file = Files.writeString(tmp.resolve("mt.txt"), MT103CONTENT);
// Path success = tmp.resolve("success");
//
// try (MockedStatic<SwiftMessageUtil> ignored = mockSwift(true)) {
// when(behavProps.isDeleteAfterProcessing()).thenReturn(false);
//
// handler.processFile(file);
//
// assertTrue(Files.exists(success.resolve(file.getFileName())));
// }
// }
//
// @Test
// void mt103_deleted_when_deleteTrue_and_presentInDb() throws Exception {
// Path file = Files.writeString(tmp.resolve("mt.txt"), MT103CONTENT);
//
// try (MockedStatic<SwiftMessageUtil> ignored = mockSwift(true)) {
// when(behavProps.isDeleteAfterProcessing()).thenReturn(true);
// when(mt103Service.isPresent(GPI_REF)).thenReturn(true);
//
// handler.processFile(file);
//
// assertFalse(Files.exists(file));
// }
// }
//
// @Test
// void mt103_kept_when_deleteTrue_but_notInDb() throws Exception {
// Path original = Files.writeString(tmp.resolve("mt.txt"), MT103CONTENT);
// try (MockedStatic<SwiftMessageUtil> ignored = mockSwift(true)) {
// when(behavProps.isDeleteAfterProcessing()).thenReturn(true);
// when(mt103Service.isPresent(GPI_REF)).thenReturn(false);
//
// handler.processFile(original);
//
// assertTrue(Files.exists(tmp.resolve("mt.txt.processing")));
// }
// }
//
// @Test
// void nonMt103_moved_to_unsuccess_when_deleteFalse() throws Exception {
// Path file = Files.writeString(tmp.resolve("bad.txt"), MT103CONTENT);
// Path unsuccess = tmp.resolve("unsuccess");
//
// try (MockedStatic<SwiftMessageUtil> ignored = mockSwift(false)) {
// when(behavProps.isDeleteAfterProcessing()).thenReturn(false);
//
// handler.processFile(file);
//
// assertTrue(Files.exists(unsuccess.resolve(file.getFileName())));
// }
// }
//
// @Test
// void nonMt103_deleted_when_deleteTrue() throws Exception {
// Path file = Files.writeString(tmp.resolve("bad.txt"), MT103CONTENT);
//
// try (MockedStatic<SwiftMessageUtil> ignored = mockSwift(false)) {
// when(behavProps.isDeleteAfterProcessing()).thenReturn(true);
//
// handler.processFile(file);
//
// assertFalse(Files.exists(file));
// }
// }
//
// @Test
// void ioException_while_moving_to_success_is_swallowed() throws Exception {
// Path file = Files.writeString(tmp.resolve("mt.txt"), MT103CONTENT);
//
// try (MockedStatic<SwiftMessageUtil> ignored1 = mockSwift(true);
// MockedStatic<Files> ignored2 = mockStatic(Files.class, CALLS_REAL_METHODS)) {
//
// when(behavProps.isDeleteAfterProcessing()).thenReturn(false);
// ignored2.when(() -> Files.move(any(), any())).thenThrow(new IOException("boom"));
//
// assertDoesNotThrow(() -> handler.processFile(file));
// }
// }
//
// @Test
// void ioException_while_moving_to_unsuccess_is_swallowed() throws Exception {
// Path file = Files.writeString(tmp.resolve("bad.txt"), MT103CONTENT);
//
// try (MockedStatic<SwiftMessageUtil> ignored1 = mockSwift(false);
// MockedStatic<Files> ignored2 = mockStatic(Files.class, CALLS_REAL_METHODS)) {
//
// when(behavProps.isDeleteAfterProcessing()).thenReturn(false);
// ignored2.when(() -> Files.move(any(), any())).thenThrow(new IOException("boom"));
//
// assertDoesNotThrow(() -> handler.processFile(file));
// }
// }
//
// @Test
// void reading_a_directory_instead_of_file_is_logged_not_thrown() throws Exception {
// Path dir = Files.createDirectory(tmp.resolve("badDir"));
// assertDoesNotThrow(() -> handler.processFile(dir));
// }
//
// @Test
// void listing_missing_directory_is_logged_not_thrown() {
// Path missing = tmp.resolve("does_not_exist");
// assertDoesNotThrow(() -> handler.processExistingFiles(missing));
// }
//
// @Test
// void consumeFilesEveryHour_delegates_to_processExistingFiles() {
// FileWatcherHandler spy = spy(handler);
// doNothing().when(spy).processExistingFiles(any());
//
// spy.consumeFilesEveryHour();
//
// verify(spy).processExistingFiles(Path.of(tmp.toString()));
// }
//
// @Test
// void watchDirectory_handles_single_create_event() throws Exception {
// WatchService watchService = mock(WatchService.class);
// WatchKey watchKey = mock(WatchKey.class);
// Path folderMock = mock(Path.class);
// Path newFileName = mock(Path.class);
//
// when(folderProps.getFolder()).thenReturn("dummy");
// when(newFileName.toString()).thenReturn("incoming.txt");
// when(newFileName.getFileName()).thenReturn(newFileName);
// when(folderMock.resolve(any(Path.class))).thenReturn(newFileName);
//
// try (MockedStatic<FileSystems> fsMock = mockStatic(FileSystems.class);
// MockedStatic<Paths> pMock = mockStatic(Paths.class)) {
//
// FileSystem fs = mock(FileSystem.class);
// fsMock.when(FileSystems::getDefault).thenReturn(fs);
// when(fs.newWatchService()).thenReturn(watchService);
// pMock.when(() -> Paths.get("dummy")).thenReturn(folderMock);
//
// doReturn(watchKey).when(folderMock)
// .register(eq(watchService), eq(StandardWatchEventKinds.ENTRY_CREATE));
//
// @SuppressWarnings("unchecked")
// WatchEvent<Path> event = mock(WatchEvent.class);
// when(event.kind()).thenReturn(StandardWatchEventKinds.ENTRY_CREATE);
// when(event.context()).thenReturn(newFileName);
//
// when(watchKey.pollEvents()).thenReturn(List.of(event));
// when(watchService.take()).thenReturn(watchKey);
// when(watchKey.reset()).thenReturn(false);
//
// FileWatcherHandler spy = spy(handler);
// doNothing().when(spy).processExistingFiles(any());
// doNothing().when(spy).processFile(newFileName);
//
// spy.watchDirectory();
//
// verify(spy).processFile(newFileName);
// }
// }
//
// @Test
// @DisplayName("hasAllowedExtension(true) для валидного расширения")
// void hasAllowedExtension_true_for_allowed() throws Exception {
// Path p = Path.of("invoice.txt");
// boolean res = invokeHasAllowedExtension(p);
// assertTrue(res);
// }
//
// @Test
// @DisplayName("hasAllowedExtension(false) для *.processing")
// void hasAllowedExtension_false_for_processing() throws Exception {
// Path p = Path.of("invoice.txt.processing");
// boolean res = invokeHasAllowedExtension(p);
// assertFalse(res);
// }
//
// @Test
// void handleFailedRead_null_is_noop() throws Exception {
// Method m = FileWatcherHandler.class
// .getDeclaredMethod("handleFailedRead", Path.class);
// m.setAccessible(true);
// assertDoesNotThrow(() -> m.invoke(handler, new Object[] {null}));
// }
//
// @Test
// void ioException_during_read_logsFail_with_nullGpi() throws Exception {
// Path file = Files.writeString(tmp.resolve("boom.txt"), "ignored");
//
// try (MockedStatic<SwiftMessageUtil> _swift =
// mockStatic(SwiftMessageUtil.class);
// MockedStatic<Files> filesStub =
// mockStatic(Files.class, CALLS_REAL_METHODS)) {
//
// filesStub.when(() -> Files.readString(file))
// .thenThrow(new IOException("disk error"));
//
// when(behavProps.isDeleteAfterProcessing()).thenReturn(false);
//
// assertDoesNotThrow(() -> handler.processFile(file));
//
// verifyNoInteractions(stepLoggerService);
// }
// }
//
// @Test
// void missing_file_is_skipped_without_side_effects() {
// Path ghost = tmp.resolve("ghost.txt");
// assertDoesNotThrow(() -> handler.processFile(ghost));
// verifyNoInteractions(processor, mt103Service, stepLoggerService);
// }
//
// @Test
// void interrupted_sleep_moves_file_to_unsuccess_when_deleteFalse() throws Exception {
// Path f = Files.writeString(tmp.resolve("int.txt"), "any");
// when(behavProps.isDeleteAfterProcessing()).thenReturn(false);
// Thread.currentThread().interrupt();
// try {
// handler.processFile(f);
// } finally {
// Thread.interrupted();
// }
// assertTrue(Files.exists(tmp.resolve("unsuccess").resolve(f.getFileName())));
// }
//
//
// @Test
// void ioException_during_read_and_deleteTrue_removes_file() throws Exception {
// Path f = Files.writeString(tmp.resolve("io.txt"), "ignored");
//
// try (MockedStatic<Files> fs = mockStatic(Files.class, CALLS_REAL_METHODS)) {
// fs.when(() -> Files.readString(f)).thenThrow(new IOException("disk"));
// when(behavProps.isDeleteAfterProcessing()).thenReturn(true);
//
// assertDoesNotThrow(() -> handler.processFile(f));
// assertFalse(Files.exists(f));
// }
// }
//
// @Test
// void deleteFile_swallows_IOException() throws Exception {
// Path f = Files.writeString(tmp.resolve("keep.txt"), MT103CONTENT);
//
// try (MockedStatic<SwiftMessageUtil> __ = mockSwift(true);
// MockedStatic<Files> fs = mockStatic(Files.class, CALLS_REAL_METHODS)) {
//
// when(behavProps.isDeleteAfterProcessing()).thenReturn(true);
// when(mt103Service.isPresent(GPI_REF)).thenReturn(true);
// fs.when(() -> Files.deleteIfExists(f)).thenThrow(new IOException("perm"));
//
// assertDoesNotThrow(() -> handler.processFile(f));
// }
// }
//
// @Test
// void watchDirectory_handles_InterruptedException_cleanly() throws Exception {
// WatchService ws = mock(WatchService.class);
// when(ws.take()).thenThrow(new InterruptedException("stop"));
//
// try (MockedStatic<FileSystems> fsMock = mockStatic(FileSystems.class);
// MockedStatic<Paths> pathMock = mockStatic(Paths.class)) {
//
// Path folder = mock(Path.class);
// WatchKey dummyKey = mock(WatchKey.class);
// when(folder.register(eq(ws), eq(StandardWatchEventKinds.ENTRY_CREATE)))
// .thenReturn(dummyKey);
//
// FileSystem fs = mock(FileSystem.class);
// when(fs.newWatchService()).thenReturn(ws);
// fsMock.when(FileSystems::getDefault).thenReturn(fs);
//
// pathMock.when(() -> Paths.get(tmp.toString())).thenReturn(folder);
// when(folderProps.getFolder()).thenReturn(tmp.toString());
//
// FileWatcherHandler spy = spy(handler);
// doNothing().when(spy).processExistingFiles(any());
//
// assertDoesNotThrow(spy::watchDirectory);
// }
// }
//
// @Test
// void watchDirectory_handles_newWatchService_IOException() {
// try (MockedStatic<FileSystems> fsMock = mockStatic(FileSystems.class);
// MockedStatic<Paths> pathMock = mockStatic(Paths.class)) {
//
// FileSystem fs = mock(FileSystem.class);
// when(fs.newWatchService()).thenThrow(new IOException("nope"));
// fsMock.when(FileSystems::getDefault).thenReturn(fs);
//
// pathMock.when(() -> Paths.get("dummy")).thenReturn(mock(Path.class));
// when(folderProps.getFolder()).thenReturn("dummy");
//
// FileWatcherHandler spy = spy(handler);
// doNothing().when(spy).processExistingFiles(any());
//
// assertDoesNotThrow(spy::watchDirectory);
// } catch (IOException e) {
// throw new RuntimeException(e);
// }
// }
//
//
// private boolean invokeHasAllowedExtension(Path p) throws Exception {
// Method m = FileWatcherHandler.class
// .getDeclaredMethod("hasAllowedExtension", Path.class);
// m.setAccessible(true);
// return (boolean) m.invoke(handler, p);
// }
Комментарии
Отправить комментарий