Enhance security and reliability across SFTP workflows
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
package com.sftp.manager.controller;
|
||||
|
||||
import com.sftp.manager.dto.ConnectionRequest;
|
||||
import com.sftp.manager.dto.DisconnectRequest;
|
||||
import com.sftp.manager.dto.ApiResponse;
|
||||
import com.sftp.manager.model.Connection;
|
||||
import com.sftp.manager.service.ConnectionService;
|
||||
import com.sftp.manager.service.SessionManager;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
public class ConnectionControllerTest {
|
||||
|
||||
private ConnectionController controller;
|
||||
private ConnectionService connectionService;
|
||||
private SessionManager sessionManager;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
controller = new ConnectionController();
|
||||
connectionService = Mockito.mock(ConnectionService.class);
|
||||
sessionManager = Mockito.mock(SessionManager.class);
|
||||
|
||||
ReflectionTestUtils.setField(controller, "connectionService", connectionService);
|
||||
ReflectionTestUtils.setField(controller, "sessionManager", sessionManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void disconnect_shouldThrowWhenSessionIdEmpty() {
|
||||
DisconnectRequest request = new DisconnectRequest();
|
||||
request.setSessionId(" ");
|
||||
|
||||
IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class,
|
||||
() -> controller.disconnect(request));
|
||||
|
||||
Assertions.assertEquals("会话ID不能为空", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listConnections_shouldReturnSuccessResponse() {
|
||||
Mockito.when(connectionService.listConnections()).thenReturn(Collections.<Connection>emptyList());
|
||||
|
||||
ApiResponse<java.util.List<Connection>> response = controller.listConnections();
|
||||
|
||||
Assertions.assertTrue(response.isSuccess());
|
||||
Assertions.assertEquals("查询成功", response.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getActiveConnections_shouldReturnSuccessResponse() {
|
||||
Mockito.when(sessionManager.getAllActiveConnections()).thenReturn(Collections.<String, Connection>emptyMap());
|
||||
|
||||
ApiResponse<Map<String, Connection>> response = controller.getActiveConnections();
|
||||
|
||||
Assertions.assertTrue(response.isSuccess());
|
||||
Assertions.assertEquals("查询成功", response.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void connect_shouldDelegateToService() throws Exception {
|
||||
ConnectionRequest request = new ConnectionRequest();
|
||||
request.setHost("127.0.0.1");
|
||||
request.setUsername("root");
|
||||
|
||||
Mockito.when(connectionService.connect(request)).thenReturn("sftp-1");
|
||||
|
||||
ApiResponse<String> response = controller.connect(request);
|
||||
|
||||
Assertions.assertTrue(response.isSuccess());
|
||||
Assertions.assertEquals("sftp-1", response.getData());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.sftp.manager.controller;
|
||||
|
||||
import com.sftp.manager.dto.ApiResponse;
|
||||
import com.sftp.manager.dto.BatchDeleteRequest;
|
||||
import com.sftp.manager.dto.BatchDeleteResult;
|
||||
import com.sftp.manager.dto.DirectoryRequest;
|
||||
import com.sftp.manager.dto.FileListRequest;
|
||||
import com.sftp.manager.service.LocalFileService;
|
||||
import com.sftp.manager.service.SessionManager;
|
||||
import com.sftp.manager.service.SftpService;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
public class FileControllerTest {
|
||||
|
||||
private FileController controller;
|
||||
private LocalFileService localFileService;
|
||||
private SftpService sftpService;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
controller = new FileController();
|
||||
localFileService = Mockito.mock(LocalFileService.class);
|
||||
sftpService = Mockito.mock(SftpService.class);
|
||||
SessionManager sessionManager = Mockito.mock(SessionManager.class);
|
||||
|
||||
ReflectionTestUtils.setField(controller, "localFileService", localFileService);
|
||||
ReflectionTestUtils.setField(controller, "sftpService", sftpService);
|
||||
ReflectionTestUtils.setField(controller, "sessionManager", sessionManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listFiles_shouldThrowWhenSessionIdEmpty() {
|
||||
FileListRequest request = new FileListRequest();
|
||||
request.setSessionId(" ");
|
||||
request.setPath("/tmp");
|
||||
|
||||
IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class,
|
||||
() -> controller.listFiles(request));
|
||||
|
||||
Assertions.assertEquals("会话ID不能为空", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createDirectory_local_shouldCallLocalService() throws Exception {
|
||||
DirectoryRequest request = new DirectoryRequest();
|
||||
request.setSessionId("local");
|
||||
request.setPath("/tmp/new-dir");
|
||||
|
||||
Mockito.when(localFileService.createDirectory("/tmp/new-dir")).thenReturn(true);
|
||||
|
||||
ApiResponse<Void> response = controller.createDirectory(request);
|
||||
|
||||
Assertions.assertTrue(response.isSuccess());
|
||||
Assertions.assertEquals("创建成功", response.getMessage());
|
||||
Mockito.verify(localFileService).createDirectory("/tmp/new-dir");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void batchDelete_nullPaths_shouldUseEmptyList() {
|
||||
BatchDeleteRequest request = new BatchDeleteRequest();
|
||||
request.setSessionId("local");
|
||||
request.setPaths(null);
|
||||
|
||||
BatchDeleteResult result = new BatchDeleteResult();
|
||||
result.setSuccessCount(0);
|
||||
result.setFailCount(0);
|
||||
result.setFailedFiles(Collections.<String>emptyList());
|
||||
|
||||
Mockito.when(localFileService.batchDelete(Mockito.anyList())).thenReturn(result);
|
||||
|
||||
ApiResponse<BatchDeleteResult> response = controller.batchDelete(request);
|
||||
|
||||
Assertions.assertTrue(response.isSuccess());
|
||||
Assertions.assertNotNull(response.getData());
|
||||
Mockito.verify(localFileService).batchDelete(Mockito.anyList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.sftp.manager.service;
|
||||
|
||||
import com.sftp.manager.dto.ConnectionRequest;
|
||||
import com.sftp.manager.model.Connection;
|
||||
import com.sftp.manager.repository.ConnectionRepository;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
public class ConnectionServiceTest {
|
||||
|
||||
private ConnectionService connectionService;
|
||||
private ConnectionRepository connectionRepository;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
connectionService = new ConnectionService();
|
||||
connectionRepository = Mockito.mock(ConnectionRepository.class);
|
||||
|
||||
ReflectionTestUtils.setField(connectionService, "connectionRepository", connectionRepository);
|
||||
ReflectionTestUtils.setField(connectionService, "sessionManager", Mockito.mock(SessionManager.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildEffectiveRequest_shouldLoadSavedCredentialsById() throws Exception {
|
||||
Connection saved = new Connection();
|
||||
saved.setId(1L);
|
||||
saved.setName("saved-conn");
|
||||
saved.setHost("10.0.0.1");
|
||||
saved.setPort(null);
|
||||
saved.setUsername("saved-user");
|
||||
saved.setPassword("saved-pass");
|
||||
saved.setPrivateKeyPath("/tmp/id_rsa");
|
||||
saved.setPassPhrase("pp");
|
||||
saved.setRootPath("/home/saved");
|
||||
|
||||
Mockito.when(connectionRepository.findById(1L)).thenReturn(Optional.of(saved));
|
||||
|
||||
ConnectionRequest request = new ConnectionRequest();
|
||||
request.setId(1L);
|
||||
request.setHost("10.0.0.2");
|
||||
|
||||
ConnectionRequest effective = invokeBuildEffectiveRequest(request);
|
||||
|
||||
Assertions.assertEquals("10.0.0.2", effective.getHost());
|
||||
Assertions.assertEquals("saved-user", effective.getUsername());
|
||||
Assertions.assertEquals("saved-pass", effective.getPassword());
|
||||
Assertions.assertEquals(Integer.valueOf(22), effective.getPort());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildEffectiveRequest_shouldFailWhenNoPasswordAndNoPrivateKey() {
|
||||
ConnectionRequest request = new ConnectionRequest();
|
||||
request.setHost("127.0.0.1");
|
||||
request.setUsername("root");
|
||||
|
||||
Exception ex = Assertions.assertThrows(Exception.class, () -> invokeBuildEffectiveRequest(request));
|
||||
Assertions.assertEquals("密码和私钥不能同时为空", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configureKnownHosts_shouldFailWhenDefaultKnownHostsMissing() {
|
||||
String oldUserHome = System.getProperty("user.home");
|
||||
try {
|
||||
Path tempHome = Files.createTempDirectory("sftp-manager-home");
|
||||
System.setProperty("user.home", tempHome.toString());
|
||||
ReflectionTestUtils.setField(connectionService, "knownHostsPath", "");
|
||||
|
||||
Exception ex = Assertions.assertThrows(Exception.class, this::invokeConfigureKnownHosts);
|
||||
Assertions.assertTrue(ex.getMessage().contains("未找到 known_hosts"));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
if (oldUserHome != null) {
|
||||
System.setProperty("user.home", oldUserHome);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ConnectionRequest invokeBuildEffectiveRequest(ConnectionRequest request) throws Exception {
|
||||
Method method = ConnectionService.class.getDeclaredMethod("buildEffectiveRequest", ConnectionRequest.class);
|
||||
method.setAccessible(true);
|
||||
try {
|
||||
return (ConnectionRequest) method.invoke(connectionService, request);
|
||||
} catch (InvocationTargetException e) {
|
||||
Throwable target = e.getTargetException();
|
||||
if (target instanceof Exception) {
|
||||
throw (Exception) target;
|
||||
}
|
||||
throw new RuntimeException(target);
|
||||
}
|
||||
}
|
||||
|
||||
private void invokeConfigureKnownHosts() throws Exception {
|
||||
Method method = ConnectionService.class.getDeclaredMethod("configureKnownHosts", com.jcraft.jsch.JSch.class);
|
||||
method.setAccessible(true);
|
||||
try {
|
||||
method.invoke(connectionService, new com.jcraft.jsch.JSch());
|
||||
} catch (InvocationTargetException e) {
|
||||
Throwable target = e.getTargetException();
|
||||
if (target instanceof Exception) {
|
||||
throw (Exception) target;
|
||||
}
|
||||
throw new RuntimeException(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.sftp.manager.service;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class LocalFileServiceTest {
|
||||
|
||||
private final LocalFileService localFileService = new LocalFileService();
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
public void createDirectory_shouldRejectParentTraversalPath() {
|
||||
String badPath = tempDir.resolve("../escape-dir").toString();
|
||||
|
||||
Exception ex = Assertions.assertThrows(Exception.class,
|
||||
() -> localFileService.createDirectory(badPath));
|
||||
|
||||
Assertions.assertTrue(ex.getMessage().contains("上级目录引用"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deleteFile_shouldRejectRootPath() {
|
||||
File[] roots = File.listRoots();
|
||||
Assertions.assertNotNull(roots);
|
||||
Assertions.assertTrue(roots.length > 0);
|
||||
|
||||
String rootPath = roots[0].getPath();
|
||||
|
||||
Exception ex = Assertions.assertThrows(Exception.class,
|
||||
() -> localFileService.deleteFile(rootPath));
|
||||
|
||||
Assertions.assertTrue(ex.getMessage().contains("根目录禁止删除"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void renameFile_shouldRejectCrossDirectoryRename() throws Exception {
|
||||
Path source = tempDir.resolve("source.txt");
|
||||
Files.write(source, "data".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
Path subDir = tempDir.resolve("sub");
|
||||
Files.createDirectories(subDir);
|
||||
Path target = subDir.resolve("target.txt");
|
||||
|
||||
Exception ex = Assertions.assertThrows(Exception.class,
|
||||
() -> localFileService.renameFile(source.toString(), target.toString()));
|
||||
|
||||
Assertions.assertTrue(ex.getMessage().contains("仅支持同目录重命名"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void renameFile_shouldAllowRenameInSameDirectory() throws Exception {
|
||||
Path source = tempDir.resolve("old-name.txt");
|
||||
Files.write(source, "data".getBytes(StandardCharsets.UTF_8));
|
||||
Path target = tempDir.resolve("new-name.txt");
|
||||
|
||||
boolean result = localFileService.renameFile(source.toString(), target.toString());
|
||||
|
||||
Assertions.assertTrue(result);
|
||||
Assertions.assertFalse(Files.exists(source));
|
||||
Assertions.assertTrue(Files.exists(target));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fileExists_shouldReturnFalseWhenPathContainsTraversal() {
|
||||
boolean exists = localFileService.fileExists("../sensitive-path");
|
||||
|
||||
Assertions.assertFalse(exists);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.sftp.manager.service;
|
||||
|
||||
import com.jcraft.jsch.ChannelSftp;
|
||||
import com.sftp.manager.model.Connection;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class SessionManagerTest {
|
||||
|
||||
@Test
|
||||
public void cleanupExpiredSessions_shouldRemoveDisconnectedSession() {
|
||||
SessionManager sessionManager = new SessionManager();
|
||||
ReflectionTestUtils.setField(sessionManager, "sessionTimeout", 60_000L);
|
||||
|
||||
ChannelSftp channel = Mockito.mock(ChannelSftp.class);
|
||||
Mockito.when(channel.isConnected()).thenReturn(false);
|
||||
|
||||
Connection connection = new Connection();
|
||||
connection.setName("test");
|
||||
|
||||
String sessionId = sessionManager.addSession(channel, connection);
|
||||
|
||||
sessionManager.cleanupExpiredSessions();
|
||||
|
||||
Assertions.assertNull(sessionManager.getSession(sessionId));
|
||||
Assertions.assertNull(sessionManager.getConnection(sessionId));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cleanupExpiredSessions_shouldRemoveTimeoutSession() {
|
||||
SessionManager sessionManager = new SessionManager();
|
||||
ReflectionTestUtils.setField(sessionManager, "sessionTimeout", 100L);
|
||||
|
||||
ChannelSftp channel = Mockito.mock(ChannelSftp.class);
|
||||
Mockito.when(channel.isConnected()).thenReturn(true);
|
||||
|
||||
Connection connection = new Connection();
|
||||
connection.setName("timeout-test");
|
||||
|
||||
String sessionId = sessionManager.addSession(channel, connection);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Long> accessMap = (Map<String, Long>) ReflectionTestUtils.getField(sessionManager, "sessionLastAccessTime");
|
||||
Assertions.assertNotNull(accessMap);
|
||||
accessMap.put(sessionId, System.currentTimeMillis() - 10_000L);
|
||||
|
||||
sessionManager.cleanupExpiredSessions();
|
||||
|
||||
Assertions.assertNull(sessionManager.getSession(sessionId));
|
||||
Assertions.assertNull(sessionManager.getConnection(sessionId));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getSession_shouldRefreshLastAccessTime() throws Exception {
|
||||
SessionManager sessionManager = new SessionManager();
|
||||
ReflectionTestUtils.setField(sessionManager, "sessionTimeout", 60_000L);
|
||||
|
||||
ChannelSftp channel = Mockito.mock(ChannelSftp.class);
|
||||
Mockito.when(channel.isConnected()).thenReturn(true);
|
||||
|
||||
Connection connection = new Connection();
|
||||
connection.setName("access-test");
|
||||
|
||||
String sessionId = sessionManager.addSession(channel, connection);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Long> accessMap = (Map<String, Long>) ReflectionTestUtils.getField(sessionManager, "sessionLastAccessTime");
|
||||
Assertions.assertNotNull(accessMap);
|
||||
accessMap.put(sessionId, 1L);
|
||||
|
||||
sessionManager.getSession(sessionId);
|
||||
|
||||
Long refreshedTime = accessMap.get(sessionId);
|
||||
Assertions.assertNotNull(refreshedTime);
|
||||
Assertions.assertTrue(refreshedTime > 1L);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user