实现快传,完善快传和网盘的功能,实现文件的互传等一系列功能
This commit is contained in:
@@ -33,9 +33,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
"spring.jpa.hibernate.ddl-auto=create-drop",
|
||||
"app.jwt.secret=0123456789abcdef0123456789abcdef",
|
||||
"app.admin.usernames=admin",
|
||||
"app.storage.root-dir=./target/test-storage-admin",
|
||||
"app.cqu.require-login=true",
|
||||
"app.cqu.mock-enabled=false"
|
||||
"app.storage.root-dir=./target/test-storage-admin"
|
||||
}
|
||||
)
|
||||
@AutoConfigureMockMvc
|
||||
@@ -66,8 +64,6 @@ class AdminControllerIntegrationTest {
|
||||
portalUser.setPhoneNumber("13800138000");
|
||||
portalUser.setPasswordHash("encoded-password");
|
||||
portalUser.setCreatedAt(LocalDateTime.now());
|
||||
portalUser.setLastSchoolStudentId("20230001");
|
||||
portalUser.setLastSchoolSemester("2025-2026-1");
|
||||
portalUser = userRepository.save(portalUser);
|
||||
|
||||
secondaryUser = new User();
|
||||
@@ -109,15 +105,13 @@ class AdminControllerIntegrationTest {
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.items[0].username").value("alice"))
|
||||
.andExpect(jsonPath("$.data.items[0].phoneNumber").value("13800138000"))
|
||||
.andExpect(jsonPath("$.data.items[0].lastSchoolStudentId").value("20230001"))
|
||||
.andExpect(jsonPath("$.data.items[0].role").value("USER"))
|
||||
.andExpect(jsonPath("$.data.items[0].banned").value(false));
|
||||
|
||||
mockMvc.perform(get("/api/admin/summary"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.totalUsers").value(2))
|
||||
.andExpect(jsonPath("$.data.totalFiles").value(2))
|
||||
.andExpect(jsonPath("$.data.usersWithSchoolCache").value(1));
|
||||
.andExpect(jsonPath("$.data.totalFiles").value(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -28,9 +28,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
"spring.datasource.password=",
|
||||
"spring.jpa.hibernate.ddl-auto=create-drop",
|
||||
"app.jwt.secret=0123456789abcdef0123456789abcdef",
|
||||
"app.storage.root-dir=./target/test-storage-refresh",
|
||||
"app.cqu.require-login=true",
|
||||
"app.cqu.mock-enabled=false"
|
||||
"app.storage.root-dir=./target/test-storage-refresh"
|
||||
}
|
||||
)
|
||||
class RefreshTokenServiceIntegrationTest {
|
||||
|
||||
@@ -28,4 +28,5 @@ class SecurityConfigTest {
|
||||
assertThat(configuration).isNotNull();
|
||||
assertThat(configuration.getAllowedMethods()).contains("PATCH");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
package com.yoyuzh.cqu;
|
||||
|
||||
import com.yoyuzh.auth.User;
|
||||
import com.yoyuzh.auth.UserRepository;
|
||||
import com.yoyuzh.config.CquApiProperties;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CquDataServiceTest {
|
||||
|
||||
@Mock
|
||||
private CquApiClient cquApiClient;
|
||||
|
||||
@Mock
|
||||
private CourseRepository courseRepository;
|
||||
|
||||
@Mock
|
||||
private GradeRepository gradeRepository;
|
||||
|
||||
@Mock
|
||||
private UserRepository userRepository;
|
||||
|
||||
@InjectMocks
|
||||
private CquDataService cquDataService;
|
||||
|
||||
@Test
|
||||
void shouldNormalizeScheduleFromRemoteApi() {
|
||||
CquApiProperties properties = new CquApiProperties();
|
||||
properties.setRequireLogin(false);
|
||||
cquDataService = new CquDataService(cquApiClient, courseRepository, gradeRepository, userRepository, properties);
|
||||
when(cquApiClient.fetchSchedule("2025-2026-1", "20230001")).thenReturn(List.of(Map.of(
|
||||
"courseName", "Java",
|
||||
"teacher", "Zhang",
|
||||
"classroom", "A101",
|
||||
"dayOfWeek", 1,
|
||||
"startTime", 1,
|
||||
"endTime", 2
|
||||
)));
|
||||
|
||||
List<CourseResponse> response = cquDataService.getSchedule(null, "2025-2026-1", "20230001");
|
||||
|
||||
assertThat(response).hasSize(1);
|
||||
assertThat(response.get(0).courseName()).isEqualTo("Java");
|
||||
assertThat(response.get(0).teacher()).isEqualTo("Zhang");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPersistGradesForLoggedInUserWhenAvailable() {
|
||||
CquApiProperties properties = new CquApiProperties();
|
||||
properties.setRequireLogin(true);
|
||||
cquDataService = new CquDataService(cquApiClient, courseRepository, gradeRepository, userRepository, properties);
|
||||
User user = new User();
|
||||
user.setId(1L);
|
||||
user.setUsername("alice");
|
||||
user.setEmail("alice@example.com");
|
||||
user.setPasswordHash("encoded");
|
||||
user.setCreatedAt(LocalDateTime.now());
|
||||
when(cquApiClient.fetchGrades("2025-2026-1", "20230001")).thenReturn(List.of(Map.of(
|
||||
"courseName", "Java",
|
||||
"grade", 95,
|
||||
"semester", "2025-2026-1"
|
||||
)));
|
||||
Grade persisted = new Grade();
|
||||
persisted.setUser(user);
|
||||
persisted.setCourseName("Java");
|
||||
persisted.setGrade(95D);
|
||||
persisted.setSemester("2025-2026-1");
|
||||
persisted.setStudentId("20230001");
|
||||
when(gradeRepository.saveAll(anyList())).thenReturn(List.of(persisted));
|
||||
when(gradeRepository.findByUserIdAndStudentIdOrderBySemesterAscGradeDesc(1L, "20230001"))
|
||||
.thenReturn(List.of(persisted));
|
||||
|
||||
List<GradeResponse> response = cquDataService.getGrades(user, "2025-2026-1", "20230001");
|
||||
|
||||
assertThat(response).hasSize(1);
|
||||
assertThat(response.get(0).grade()).isEqualTo(95D);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnPersistedScheduleWithoutCallingRemoteApiWhenRefreshIsDisabled() {
|
||||
CquApiProperties properties = new CquApiProperties();
|
||||
properties.setRequireLogin(true);
|
||||
cquDataService = new CquDataService(cquApiClient, courseRepository, gradeRepository, userRepository, properties);
|
||||
|
||||
User user = new User();
|
||||
user.setId(1L);
|
||||
user.setUsername("alice");
|
||||
|
||||
Course persisted = new Course();
|
||||
persisted.setUser(user);
|
||||
persisted.setCourseName("Java");
|
||||
persisted.setTeacher("Zhang");
|
||||
persisted.setClassroom("A101");
|
||||
persisted.setDayOfWeek(1);
|
||||
persisted.setStartTime(1);
|
||||
persisted.setEndTime(2);
|
||||
persisted.setSemester("2025-spring");
|
||||
persisted.setStudentId("20230001");
|
||||
|
||||
when(courseRepository.findByUserIdAndStudentIdAndSemesterOrderByDayOfWeekAscStartTimeAsc(1L, "20230001", "2025-spring"))
|
||||
.thenReturn(List.of(persisted));
|
||||
|
||||
List<CourseResponse> response = cquDataService.getSchedule(user, "2025-spring", "20230001", false);
|
||||
|
||||
assertThat(response).extracting(CourseResponse::courseName).containsExactly("Java");
|
||||
verifyNoInteractions(cquApiClient);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnLatestStoredSchoolDataFromPersistedUserContext() {
|
||||
CquApiProperties properties = new CquApiProperties();
|
||||
properties.setRequireLogin(true);
|
||||
cquDataService = new CquDataService(cquApiClient, courseRepository, gradeRepository, userRepository, properties);
|
||||
|
||||
User user = new User();
|
||||
user.setId(1L);
|
||||
user.setUsername("alice");
|
||||
user.setLastSchoolStudentId("20230001");
|
||||
user.setLastSchoolSemester("2025-spring");
|
||||
|
||||
Course course = new Course();
|
||||
course.setUser(user);
|
||||
course.setCourseName("Java");
|
||||
course.setTeacher("Zhang");
|
||||
course.setClassroom("A101");
|
||||
course.setDayOfWeek(1);
|
||||
course.setStartTime(1);
|
||||
course.setEndTime(2);
|
||||
course.setSemester("2025-spring");
|
||||
course.setStudentId("20230001");
|
||||
|
||||
Grade grade = new Grade();
|
||||
grade.setUser(user);
|
||||
grade.setCourseName("Java");
|
||||
grade.setGrade(95D);
|
||||
grade.setSemester("2025-spring");
|
||||
grade.setStudentId("20230001");
|
||||
|
||||
when(courseRepository.findByUserIdAndStudentIdAndSemesterOrderByDayOfWeekAscStartTimeAsc(1L, "20230001", "2025-spring"))
|
||||
.thenReturn(List.of(course));
|
||||
when(gradeRepository.findByUserIdAndStudentIdOrderBySemesterAscGradeDesc(1L, "20230001"))
|
||||
.thenReturn(List.of(grade));
|
||||
|
||||
LatestSchoolDataResponse response = cquDataService.getLatest(user);
|
||||
|
||||
assertThat(response.studentId()).isEqualTo("20230001");
|
||||
assertThat(response.semester()).isEqualTo("2025-spring");
|
||||
assertThat(response.schedule()).extracting(CourseResponse::courseName).containsExactly("Java");
|
||||
assertThat(response.grades()).extracting(GradeResponse::courseName).containsExactly("Java");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFallbackToMostRecentStoredSchoolDataWhenUserContextIsEmpty() {
|
||||
CquApiProperties properties = new CquApiProperties();
|
||||
properties.setRequireLogin(true);
|
||||
cquDataService = new CquDataService(cquApiClient, courseRepository, gradeRepository, userRepository, properties);
|
||||
|
||||
User user = new User();
|
||||
user.setId(1L);
|
||||
user.setUsername("alice");
|
||||
|
||||
Course latestCourse = new Course();
|
||||
latestCourse.setUser(user);
|
||||
latestCourse.setCourseName("Java");
|
||||
latestCourse.setTeacher("Zhang");
|
||||
latestCourse.setClassroom("A101");
|
||||
latestCourse.setDayOfWeek(1);
|
||||
latestCourse.setStartTime(1);
|
||||
latestCourse.setEndTime(2);
|
||||
latestCourse.setSemester("2025-spring");
|
||||
latestCourse.setStudentId("20230001");
|
||||
latestCourse.setCreatedAt(LocalDateTime.now());
|
||||
|
||||
when(courseRepository.findTopByUserIdOrderByCreatedAtDesc(1L)).thenReturn(Optional.of(latestCourse));
|
||||
when(gradeRepository.findTopByUserIdOrderByCreatedAtDesc(1L)).thenReturn(Optional.empty());
|
||||
when(courseRepository.findByUserIdAndStudentIdAndSemesterOrderByDayOfWeekAscStartTimeAsc(1L, "20230001", "2025-spring"))
|
||||
.thenReturn(List.of(latestCourse));
|
||||
when(gradeRepository.findByUserIdAndStudentIdOrderBySemesterAscGradeDesc(1L, "20230001"))
|
||||
.thenReturn(List.of());
|
||||
|
||||
LatestSchoolDataResponse response = cquDataService.getLatest(user);
|
||||
|
||||
assertThat(response.studentId()).isEqualTo("20230001");
|
||||
assertThat(response.semester()).isEqualTo("2025-spring");
|
||||
assertThat(user.getLastSchoolStudentId()).isEqualTo("20230001");
|
||||
assertThat(user.getLastSchoolSemester()).isEqualTo("2025-spring");
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package com.yoyuzh.cqu;
|
||||
|
||||
import com.yoyuzh.PortalBackendApplication;
|
||||
import com.yoyuzh.auth.User;
|
||||
import com.yoyuzh.auth.UserRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SpringBootTest(
|
||||
classes = PortalBackendApplication.class,
|
||||
properties = {
|
||||
"spring.datasource.url=jdbc:h2:mem:cqu_tx_test;MODE=MySQL;DB_CLOSE_DELAY=-1",
|
||||
"spring.datasource.driver-class-name=org.h2.Driver",
|
||||
"spring.datasource.username=sa",
|
||||
"spring.datasource.password=",
|
||||
"spring.jpa.hibernate.ddl-auto=create-drop",
|
||||
"app.jwt.secret=0123456789abcdef0123456789abcdef",
|
||||
"app.cqu.require-login=true",
|
||||
"app.cqu.mock-enabled=false"
|
||||
}
|
||||
)
|
||||
class CquDataServiceTransactionTest {
|
||||
|
||||
@Autowired
|
||||
private CquDataService cquDataService;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private GradeRepository gradeRepository;
|
||||
|
||||
@MockBean
|
||||
private CquApiClient cquApiClient;
|
||||
|
||||
@Test
|
||||
void shouldPersistGradesInsideTransactionForLoggedInUser() {
|
||||
User user = new User();
|
||||
user.setUsername("portal-demo");
|
||||
user.setEmail("portal-demo@example.com");
|
||||
user.setPasswordHash("encoded");
|
||||
user = userRepository.save(user);
|
||||
|
||||
Grade existing = new Grade();
|
||||
existing.setUser(user);
|
||||
existing.setCourseName("Old Java");
|
||||
existing.setGrade(60D);
|
||||
existing.setSemester("2025-spring");
|
||||
existing.setStudentId("2023123456");
|
||||
gradeRepository.save(existing);
|
||||
|
||||
when(cquApiClient.fetchGrades("2025-spring", "2023123456")).thenReturn(List.of(
|
||||
Map.of(
|
||||
"courseName", "Java",
|
||||
"grade", 95,
|
||||
"semester", "2025-spring"
|
||||
)
|
||||
));
|
||||
|
||||
List<GradeResponse> response = cquDataService.getGrades(user, "2025-spring", "2023123456", true);
|
||||
|
||||
assertThat(response).hasSize(1);
|
||||
assertThat(response.get(0).courseName()).isEqualTo("Java");
|
||||
assertThat(response.get(0).grade()).isEqualTo(95D);
|
||||
assertThat(gradeRepository.findByUserIdAndStudentIdOrderBySemesterAscGradeDesc(user.getId(), "2023123456"))
|
||||
.hasSize(1)
|
||||
.first()
|
||||
.extracting(Grade::getCourseName)
|
||||
.isEqualTo("Java");
|
||||
assertThat(userRepository.findById(user.getId()))
|
||||
.get()
|
||||
.extracting(User::getLastSchoolStudentId, User::getLastSchoolSemester)
|
||||
.containsExactly("2023123456", "2025-spring");
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package com.yoyuzh.cqu;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class CquMockDataFactoryTest {
|
||||
|
||||
@Test
|
||||
void shouldCreateMockScheduleForStudentAndSemester() {
|
||||
List<Map<String, Object>> result = CquMockDataFactory.createSchedule("2025-2026-1", "20230001");
|
||||
|
||||
assertThat(result).isNotEmpty();
|
||||
assertThat(result.get(0)).containsEntry("courseName", "高级 Java 程序设计");
|
||||
assertThat(result.get(0)).containsEntry("semester", "2025-2026-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateMockGradesForStudentAndSemester() {
|
||||
List<Map<String, Object>> result = CquMockDataFactory.createGrades("2025-2026-1", "20230001");
|
||||
|
||||
assertThat(result).isNotEmpty();
|
||||
assertThat(result.get(0)).containsEntry("studentId", "20230001");
|
||||
assertThat(result.get(0)).containsKey("grade");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnDifferentMockDataForDifferentStudents() {
|
||||
List<Map<String, Object>> firstSchedule = CquMockDataFactory.createSchedule("2025-2026-1", "2023123456");
|
||||
List<Map<String, Object>> secondSchedule = CquMockDataFactory.createSchedule("2025-2026-1", "2022456789");
|
||||
List<Map<String, Object>> firstGrades = CquMockDataFactory.createGrades("2025-2026-1", "2023123456");
|
||||
List<Map<String, Object>> secondGrades = CquMockDataFactory.createGrades("2025-2026-1", "2022456789");
|
||||
|
||||
assertThat(firstSchedule).extracting(item -> item.get("courseName"))
|
||||
.isNotEqualTo(secondSchedule.stream().map(item -> item.get("courseName")).toList());
|
||||
assertThat(firstGrades).extracting(item -> item.get("grade"))
|
||||
.isNotEqualTo(secondGrades.stream().map(item -> item.get("grade")).toList());
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import java.util.zip.ZipInputStream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.AdditionalMatchers.aryEq;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
@@ -43,13 +44,16 @@ class FileServiceTest {
|
||||
@Mock
|
||||
private FileContentStorage fileContentStorage;
|
||||
|
||||
@Mock
|
||||
private FileShareLinkRepository fileShareLinkRepository;
|
||||
|
||||
private FileService fileService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
FileStorageProperties properties = new FileStorageProperties();
|
||||
properties.setMaxFileSize(500L * 1024 * 1024);
|
||||
fileService = new FileService(storedFileRepository, fileContentStorage, properties);
|
||||
fileService = new FileService(storedFileRepository, fileContentStorage, fileShareLinkRepository, properties);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -167,6 +171,140 @@ class FileServiceTest {
|
||||
verify(fileContentStorage).renameDirectory(7L, "/docs/archive", "/docs/renamed-archive", List.of(childFile));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMoveFileToAnotherDirectory() {
|
||||
User user = createUser(7L);
|
||||
StoredFile file = createFile(10L, user, "/docs", "notes.txt");
|
||||
StoredFile targetDirectory = createDirectory(11L, user, "/", "下载");
|
||||
when(storedFileRepository.findById(10L)).thenReturn(Optional.of(file));
|
||||
when(storedFileRepository.findByUserIdAndPathAndFilename(7L, "/", "下载")).thenReturn(Optional.of(targetDirectory));
|
||||
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/下载", "notes.txt")).thenReturn(false);
|
||||
when(storedFileRepository.save(any(StoredFile.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
FileMetadataResponse response = fileService.move(user, 10L, "/下载");
|
||||
|
||||
assertThat(response.path()).isEqualTo("/下载");
|
||||
assertThat(file.getPath()).isEqualTo("/下载");
|
||||
verify(fileContentStorage).moveFile(7L, "/docs", "/下载", "notes.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMoveDirectoryAndUpdateDescendantPaths() {
|
||||
User user = createUser(7L);
|
||||
StoredFile directory = createDirectory(10L, user, "/docs", "archive");
|
||||
StoredFile targetDirectory = createDirectory(11L, user, "/", "图片");
|
||||
StoredFile childFile = createFile(12L, user, "/docs/archive", "nested.txt");
|
||||
when(storedFileRepository.findById(10L)).thenReturn(Optional.of(directory));
|
||||
when(storedFileRepository.findByUserIdAndPathAndFilename(7L, "/", "图片")).thenReturn(Optional.of(targetDirectory));
|
||||
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/图片", "archive")).thenReturn(false);
|
||||
when(storedFileRepository.findByUserIdAndPathEqualsOrDescendant(7L, "/docs/archive")).thenReturn(List.of(childFile));
|
||||
when(storedFileRepository.save(any(StoredFile.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(storedFileRepository.saveAll(List.of(childFile))).thenReturn(List.of(childFile));
|
||||
|
||||
FileMetadataResponse response = fileService.move(user, 10L, "/图片");
|
||||
|
||||
assertThat(response.path()).isEqualTo("/图片/archive");
|
||||
assertThat(directory.getPath()).isEqualTo("/图片");
|
||||
assertThat(childFile.getPath()).isEqualTo("/图片/archive");
|
||||
verify(fileContentStorage).renameDirectory(7L, "/docs/archive", "/图片/archive", List.of(childFile));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectMovingDirectoryIntoItsOwnDescendant() {
|
||||
User user = createUser(7L);
|
||||
StoredFile directory = createDirectory(10L, user, "/docs", "archive");
|
||||
StoredFile docsDirectory = createDirectory(11L, user, "/", "docs");
|
||||
StoredFile archiveDirectory = createDirectory(12L, user, "/docs", "archive");
|
||||
StoredFile descendantDirectory = createDirectory(13L, user, "/docs/archive", "nested");
|
||||
when(storedFileRepository.findById(10L)).thenReturn(Optional.of(directory));
|
||||
when(storedFileRepository.findByUserIdAndPathAndFilename(7L, "/", "docs"))
|
||||
.thenReturn(Optional.of(docsDirectory));
|
||||
when(storedFileRepository.findByUserIdAndPathAndFilename(7L, "/docs", "archive"))
|
||||
.thenReturn(Optional.of(archiveDirectory));
|
||||
when(storedFileRepository.findByUserIdAndPathAndFilename(7L, "/docs/archive", "nested"))
|
||||
.thenReturn(Optional.of(descendantDirectory));
|
||||
|
||||
assertThatThrownBy(() -> fileService.move(user, 10L, "/docs/archive/nested"))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("不能移动到当前目录或其子目录");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCopyFileToAnotherDirectory() {
|
||||
User user = createUser(7L);
|
||||
StoredFile file = createFile(10L, user, "/docs", "notes.txt");
|
||||
StoredFile targetDirectory = createDirectory(11L, user, "/", "下载");
|
||||
when(storedFileRepository.findById(10L)).thenReturn(Optional.of(file));
|
||||
when(storedFileRepository.findByUserIdAndPathAndFilename(7L, "/", "下载")).thenReturn(Optional.of(targetDirectory));
|
||||
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/下载", "notes.txt")).thenReturn(false);
|
||||
when(storedFileRepository.save(any(StoredFile.class))).thenAnswer(invocation -> {
|
||||
StoredFile storedFile = invocation.getArgument(0);
|
||||
if (storedFile.getId() == null) {
|
||||
storedFile.setId(20L);
|
||||
}
|
||||
return storedFile;
|
||||
});
|
||||
|
||||
FileMetadataResponse response = fileService.copy(user, 10L, "/下载");
|
||||
|
||||
assertThat(response.id()).isEqualTo(20L);
|
||||
assertThat(response.path()).isEqualTo("/下载");
|
||||
verify(fileContentStorage).copyFile(7L, "/docs", "/下载", "notes.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCopyDirectoryAndDescendants() {
|
||||
User user = createUser(7L);
|
||||
StoredFile directory = createDirectory(10L, user, "/docs", "archive");
|
||||
StoredFile targetDirectory = createDirectory(11L, user, "/", "图片");
|
||||
StoredFile childDirectory = createDirectory(12L, user, "/docs/archive", "nested");
|
||||
StoredFile childFile = createFile(13L, user, "/docs/archive", "notes.txt");
|
||||
StoredFile nestedFile = createFile(14L, user, "/docs/archive/nested", "todo.txt");
|
||||
when(storedFileRepository.findById(10L)).thenReturn(Optional.of(directory));
|
||||
when(storedFileRepository.findByUserIdAndPathAndFilename(7L, "/", "图片")).thenReturn(Optional.of(targetDirectory));
|
||||
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/图片", "archive")).thenReturn(false);
|
||||
when(storedFileRepository.findByUserIdAndPathEqualsOrDescendant(7L, "/docs/archive"))
|
||||
.thenReturn(List.of(childDirectory, childFile, nestedFile));
|
||||
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/图片/archive", "nested")).thenReturn(false);
|
||||
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/图片/archive", "notes.txt")).thenReturn(false);
|
||||
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/图片/archive/nested", "todo.txt")).thenReturn(false);
|
||||
when(storedFileRepository.save(any(StoredFile.class))).thenAnswer(invocation -> {
|
||||
StoredFile storedFile = invocation.getArgument(0);
|
||||
if (storedFile.getId() == null) {
|
||||
storedFile.setId(100L + storedFile.getFilename().length());
|
||||
}
|
||||
return storedFile;
|
||||
});
|
||||
|
||||
FileMetadataResponse response = fileService.copy(user, 10L, "/图片");
|
||||
|
||||
assertThat(response.path()).isEqualTo("/图片/archive");
|
||||
verify(fileContentStorage).ensureDirectory(7L, "/图片/archive");
|
||||
verify(fileContentStorage).ensureDirectory(7L, "/图片/archive/nested");
|
||||
verify(fileContentStorage).copyFile(7L, "/docs/archive", "/图片/archive", "notes.txt");
|
||||
verify(fileContentStorage).copyFile(7L, "/docs/archive/nested", "/图片/archive/nested", "todo.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectCopyingDirectoryIntoItsOwnDescendant() {
|
||||
User user = createUser(7L);
|
||||
StoredFile directory = createDirectory(10L, user, "/docs", "archive");
|
||||
StoredFile docsDirectory = createDirectory(11L, user, "/", "docs");
|
||||
StoredFile archiveDirectory = createDirectory(12L, user, "/docs", "archive");
|
||||
StoredFile descendantDirectory = createDirectory(13L, user, "/docs/archive", "nested");
|
||||
when(storedFileRepository.findById(10L)).thenReturn(Optional.of(directory));
|
||||
when(storedFileRepository.findByUserIdAndPathAndFilename(7L, "/", "docs"))
|
||||
.thenReturn(Optional.of(docsDirectory));
|
||||
when(storedFileRepository.findByUserIdAndPathAndFilename(7L, "/docs", "archive"))
|
||||
.thenReturn(Optional.of(archiveDirectory));
|
||||
when(storedFileRepository.findByUserIdAndPathAndFilename(7L, "/docs/archive", "nested"))
|
||||
.thenReturn(Optional.of(descendantDirectory));
|
||||
|
||||
assertThatThrownBy(() -> fileService.copy(user, 10L, "/docs/archive/nested"))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("不能复制到当前目录或其子目录");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectDeletingOtherUsersFile() {
|
||||
User owner = createUser(1L);
|
||||
@@ -293,6 +431,60 @@ class FileServiceTest {
|
||||
verify(fileContentStorage).readFile(7L, "/docs/archive/nested", "todo.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateShareLinkForOwnedFile() {
|
||||
User user = createUser(7L);
|
||||
StoredFile file = createFile(22L, user, "/docs", "notes.txt");
|
||||
when(storedFileRepository.findById(22L)).thenReturn(Optional.of(file));
|
||||
when(fileShareLinkRepository.save(any(FileShareLink.class))).thenAnswer(invocation -> {
|
||||
FileShareLink shareLink = invocation.getArgument(0);
|
||||
shareLink.setId(100L);
|
||||
shareLink.setToken("share-token-1");
|
||||
return shareLink;
|
||||
});
|
||||
|
||||
CreateFileShareLinkResponse response = fileService.createShareLink(user, 22L);
|
||||
|
||||
assertThat(response.token()).isEqualTo("share-token-1");
|
||||
assertThat(response.filename()).isEqualTo("notes.txt");
|
||||
verify(fileShareLinkRepository).save(any(FileShareLink.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldImportSharedFileIntoRecipientWorkspace() {
|
||||
User owner = createUser(7L);
|
||||
User recipient = createUser(8L);
|
||||
StoredFile sourceFile = createFile(22L, owner, "/docs", "notes.txt");
|
||||
FileShareLink shareLink = new FileShareLink();
|
||||
shareLink.setId(100L);
|
||||
shareLink.setToken("share-token-1");
|
||||
shareLink.setOwner(owner);
|
||||
shareLink.setFile(sourceFile);
|
||||
shareLink.setCreatedAt(LocalDateTime.now());
|
||||
when(fileShareLinkRepository.findByToken("share-token-1")).thenReturn(Optional.of(shareLink));
|
||||
when(storedFileRepository.existsByUserIdAndPathAndFilename(8L, "/下载", "notes.txt")).thenReturn(false);
|
||||
when(storedFileRepository.save(any(StoredFile.class))).thenAnswer(invocation -> {
|
||||
StoredFile file = invocation.getArgument(0);
|
||||
file.setId(200L);
|
||||
return file;
|
||||
});
|
||||
when(fileContentStorage.readFile(7L, "/docs", "notes.txt"))
|
||||
.thenReturn("hello".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
FileMetadataResponse response = fileService.importSharedFile(recipient, "share-token-1", "/下载");
|
||||
|
||||
assertThat(response.id()).isEqualTo(200L);
|
||||
assertThat(response.path()).isEqualTo("/下载");
|
||||
assertThat(response.filename()).isEqualTo("notes.txt");
|
||||
verify(fileContentStorage).storeImportedFile(
|
||||
eq(8L),
|
||||
eq("/下载"),
|
||||
eq("notes.txt"),
|
||||
eq(sourceFile.getContentType()),
|
||||
aryEq("hello".getBytes(StandardCharsets.UTF_8))
|
||||
);
|
||||
}
|
||||
|
||||
private User createUser(Long id) {
|
||||
User user = new User();
|
||||
user.setId(id);
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
package com.yoyuzh.files;
|
||||
|
||||
import com.yoyuzh.PortalBackendApplication;
|
||||
import com.yoyuzh.auth.User;
|
||||
import com.yoyuzh.auth.UserRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest(
|
||||
classes = PortalBackendApplication.class,
|
||||
properties = {
|
||||
"spring.datasource.url=jdbc:h2:mem:file_share_api_test;MODE=MySQL;DB_CLOSE_DELAY=-1",
|
||||
"spring.datasource.driver-class-name=org.h2.Driver",
|
||||
"spring.datasource.username=sa",
|
||||
"spring.datasource.password=",
|
||||
"spring.jpa.hibernate.ddl-auto=create-drop",
|
||||
"app.jwt.secret=0123456789abcdef0123456789abcdef",
|
||||
"app.storage.root-dir=./target/test-storage-file-share"
|
||||
}
|
||||
)
|
||||
@AutoConfigureMockMvc
|
||||
class FileShareControllerIntegrationTest {
|
||||
|
||||
private static final Path STORAGE_ROOT = Path.of("./target/test-storage-file-share").toAbsolutePath().normalize();
|
||||
private Long sharedFileId;
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private StoredFileRepository storedFileRepository;
|
||||
|
||||
@Autowired
|
||||
private FileShareLinkRepository fileShareLinkRepository;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
fileShareLinkRepository.deleteAll();
|
||||
storedFileRepository.deleteAll();
|
||||
userRepository.deleteAll();
|
||||
if (Files.exists(STORAGE_ROOT)) {
|
||||
try (var paths = Files.walk(STORAGE_ROOT)) {
|
||||
paths.sorted((left, right) -> right.compareTo(left)).forEach(path -> {
|
||||
try {
|
||||
Files.deleteIfExists(path);
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Files.createDirectories(STORAGE_ROOT);
|
||||
|
||||
User owner = new User();
|
||||
owner.setUsername("alice");
|
||||
owner.setEmail("alice@example.com");
|
||||
owner.setPhoneNumber("13800138000");
|
||||
owner.setPasswordHash("encoded-password");
|
||||
owner.setCreatedAt(LocalDateTime.now());
|
||||
owner = userRepository.save(owner);
|
||||
|
||||
User recipient = new User();
|
||||
recipient.setUsername("bob");
|
||||
recipient.setEmail("bob@example.com");
|
||||
recipient.setPhoneNumber("13800138001");
|
||||
recipient.setPasswordHash("encoded-password");
|
||||
recipient.setCreatedAt(LocalDateTime.now());
|
||||
recipient = userRepository.save(recipient);
|
||||
|
||||
StoredFile file = new StoredFile();
|
||||
file.setUser(owner);
|
||||
file.setFilename("notes.txt");
|
||||
file.setPath("/docs");
|
||||
file.setStorageName("notes.txt");
|
||||
file.setContentType("text/plain");
|
||||
file.setSize(5L);
|
||||
file.setDirectory(false);
|
||||
sharedFileId = storedFileRepository.save(file).getId();
|
||||
|
||||
Path ownerDir = STORAGE_ROOT.resolve(owner.getId().toString()).resolve("docs");
|
||||
Files.createDirectories(ownerDir);
|
||||
Files.writeString(ownerDir.resolve("notes.txt"), "hello", StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateInspectAndImportSharedFile() throws Exception {
|
||||
String response = mockMvc.perform(post("/api/files/{fileId}/share-links", sharedFileId)
|
||||
.with(user("alice")))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.token").isNotEmpty())
|
||||
.andExpect(jsonPath("$.data.filename").value("notes.txt"))
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
|
||||
String token = com.jayway.jsonpath.JsonPath.read(response, "$.data.token");
|
||||
|
||||
mockMvc.perform(get("/api/files/share-links/{token}", token))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.token").value(token))
|
||||
.andExpect(jsonPath("$.data.filename").value("notes.txt"))
|
||||
.andExpect(jsonPath("$.data.ownerUsername").value("alice"));
|
||||
|
||||
mockMvc.perform(post("/api/files/share-links/{token}/import", token)
|
||||
.with(anonymous())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"path": "/下载"
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isUnauthorized());
|
||||
|
||||
mockMvc.perform(post("/api/files/share-links/{token}/import", token)
|
||||
.with(user("bob"))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"path": "/下载"
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.filename").value("notes.txt"))
|
||||
.andExpect(jsonPath("$.data.path").value("/下载"));
|
||||
|
||||
mockMvc.perform(get("/api/files/list")
|
||||
.with(user("bob"))
|
||||
.param("path", "/下载")
|
||||
.param("page", "0")
|
||||
.param("size", "20"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.items[0].filename").value("notes.txt"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMoveFileIntoAnotherDirectoryThroughApi() throws Exception {
|
||||
User owner = userRepository.findByUsername("alice").orElseThrow();
|
||||
|
||||
StoredFile downloadDirectory = new StoredFile();
|
||||
downloadDirectory.setUser(owner);
|
||||
downloadDirectory.setFilename("下载");
|
||||
downloadDirectory.setPath("/");
|
||||
downloadDirectory.setStorageName("下载");
|
||||
downloadDirectory.setContentType("directory");
|
||||
downloadDirectory.setSize(0L);
|
||||
downloadDirectory.setDirectory(true);
|
||||
storedFileRepository.save(downloadDirectory);
|
||||
Files.createDirectories(STORAGE_ROOT.resolve(owner.getId().toString()).resolve("下载"));
|
||||
|
||||
mockMvc.perform(patch("/api/files/{fileId}/move", sharedFileId)
|
||||
.with(user("alice"))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"path": "/下载"
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.filename").value("notes.txt"))
|
||||
.andExpect(jsonPath("$.data.path").value("/下载"));
|
||||
|
||||
mockMvc.perform(get("/api/files/list")
|
||||
.with(user("alice"))
|
||||
.param("path", "/下载")
|
||||
.param("page", "0")
|
||||
.param("size", "20"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.items[0].filename").value("notes.txt"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCopyFileIntoAnotherDirectoryThroughApi() throws Exception {
|
||||
User owner = userRepository.findByUsername("alice").orElseThrow();
|
||||
|
||||
StoredFile downloadDirectory = new StoredFile();
|
||||
downloadDirectory.setUser(owner);
|
||||
downloadDirectory.setFilename("下载");
|
||||
downloadDirectory.setPath("/");
|
||||
downloadDirectory.setStorageName("下载");
|
||||
downloadDirectory.setContentType("directory");
|
||||
downloadDirectory.setSize(0L);
|
||||
downloadDirectory.setDirectory(true);
|
||||
storedFileRepository.save(downloadDirectory);
|
||||
Files.createDirectories(STORAGE_ROOT.resolve(owner.getId().toString()).resolve("下载"));
|
||||
|
||||
mockMvc.perform(post("/api/files/{fileId}/copy", sharedFileId)
|
||||
.with(user("alice"))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"path": "/下载"
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.filename").value("notes.txt"))
|
||||
.andExpect(jsonPath("$.data.path").value("/下载"));
|
||||
|
||||
mockMvc.perform(get("/api/files/list")
|
||||
.with(user("alice"))
|
||||
.param("path", "/下载")
|
||||
.param("page", "0")
|
||||
.param("size", "20"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.items[0].filename").value("notes.txt"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.yoyuzh.transfer;
|
||||
|
||||
import com.yoyuzh.PortalBackendApplication;
|
||||
import com.yoyuzh.auth.User;
|
||||
import com.yoyuzh.auth.UserRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest(
|
||||
classes = PortalBackendApplication.class,
|
||||
properties = {
|
||||
"spring.datasource.url=jdbc:h2:mem:transfer_api_test;MODE=MySQL;DB_CLOSE_DELAY=-1",
|
||||
"spring.datasource.driver-class-name=org.h2.Driver",
|
||||
"spring.datasource.username=sa",
|
||||
"spring.datasource.password=",
|
||||
"spring.jpa.hibernate.ddl-auto=create-drop",
|
||||
"app.jwt.secret=0123456789abcdef0123456789abcdef",
|
||||
"app.storage.root-dir=./target/test-storage-transfer"
|
||||
}
|
||||
)
|
||||
@AutoConfigureMockMvc
|
||||
class TransferControllerIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
userRepository.deleteAll();
|
||||
|
||||
User portalUser = new User();
|
||||
portalUser.setUsername("alice");
|
||||
portalUser.setEmail("alice@example.com");
|
||||
portalUser.setPhoneNumber("13800138000");
|
||||
portalUser.setPasswordHash("encoded-password");
|
||||
portalUser.setCreatedAt(LocalDateTime.now());
|
||||
userRepository.save(portalUser);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "alice")
|
||||
void shouldCreateLookupJoinAndPollTransferSignals() throws Exception {
|
||||
String response = mockMvc.perform(post("/api/transfer/sessions")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"files": [
|
||||
{"name": "report.pdf", "size": 2048, "contentType": "application/pdf"}
|
||||
]
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.sessionId").isNotEmpty())
|
||||
.andExpect(jsonPath("$.data.pickupCode").isString())
|
||||
.andExpect(jsonPath("$.data.files[0].name").value("report.pdf"))
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
|
||||
String sessionId = com.jayway.jsonpath.JsonPath.read(response, "$.data.sessionId");
|
||||
String pickupCode = com.jayway.jsonpath.JsonPath.read(response, "$.data.pickupCode");
|
||||
|
||||
mockMvc.perform(get("/api/transfer/sessions/lookup").param("pickupCode", pickupCode))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.sessionId").value(sessionId))
|
||||
.andExpect(jsonPath("$.data.pickupCode").value(pickupCode));
|
||||
|
||||
mockMvc.perform(post("/api/transfer/sessions/{sessionId}/join", sessionId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.sessionId").value(sessionId))
|
||||
.andExpect(jsonPath("$.data.files[0].name").value("report.pdf"));
|
||||
|
||||
mockMvc.perform(post("/api/transfer/sessions/{sessionId}/signals", sessionId)
|
||||
.param("role", "sender")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"type": "offer",
|
||||
"payload": "{\\\"sdp\\\":\\\"demo-offer\\\"}"
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
|
||||
mockMvc.perform(get("/api/transfer/sessions/{sessionId}/signals", sessionId)
|
||||
.param("role", "receiver")
|
||||
.param("after", "0"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.items[0].type").value("offer"))
|
||||
.andExpect(jsonPath("$.data.items[0].payload").value("{\"sdp\":\"demo-offer\"}"))
|
||||
.andExpect(jsonPath("$.data.nextCursor").value(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectAnonymousSessionCreationButAllowPublicJoinEndpoints() throws Exception {
|
||||
mockMvc.perform(post("/api/transfer/sessions")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"files":[{"name":"demo.txt","size":12,"contentType":"text/plain"}]}
|
||||
"""))
|
||||
.andExpect(status().isUnauthorized());
|
||||
|
||||
mockMvc.perform(post("/api/transfer/sessions/{sessionId}/join", "missing-session"))
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.yoyuzh.transfer;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class TransferSessionTest {
|
||||
|
||||
@Test
|
||||
void shouldEmitPeerJoinedOnlyOnceWhenReceiverJoinsRepeatedly() {
|
||||
TransferSession session = new TransferSession(
|
||||
"session-1",
|
||||
"849201",
|
||||
Instant.parse("2026-03-20T12:00:00Z"),
|
||||
List.of(new TransferFileItem("report.pdf", 2048, "application/pdf"))
|
||||
);
|
||||
|
||||
session.markReceiverJoined();
|
||||
session.markReceiverJoined();
|
||||
|
||||
PollTransferSignalsResponse senderSignals = session.poll(TransferRole.SENDER, 0);
|
||||
|
||||
assertThat(senderSignals.items())
|
||||
.extracting(TransferSignalEnvelope::type)
|
||||
.containsExactly("peer-joined");
|
||||
assertThat(senderSignals.nextCursor()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRouteSignalsToTheOppositeRoleQueue() {
|
||||
TransferSession session = new TransferSession(
|
||||
"session-1",
|
||||
"849201",
|
||||
Instant.parse("2026-03-20T12:00:00Z"),
|
||||
List.of(new TransferFileItem("report.pdf", 2048, "application/pdf"))
|
||||
);
|
||||
|
||||
session.enqueue(TransferRole.SENDER, "offer", "{\"sdp\":\"demo-offer\"}");
|
||||
session.enqueue(TransferRole.RECEIVER, "answer", "{\"sdp\":\"demo-answer\"}");
|
||||
|
||||
PollTransferSignalsResponse receiverSignals = session.poll(TransferRole.RECEIVER, 0);
|
||||
PollTransferSignalsResponse senderSignals = session.poll(TransferRole.SENDER, 0);
|
||||
|
||||
assertThat(receiverSignals.items())
|
||||
.extracting(TransferSignalEnvelope::type, TransferSignalEnvelope::payload)
|
||||
.containsExactly(org.assertj.core.groups.Tuple.tuple("offer", "{\"sdp\":\"demo-offer\"}"));
|
||||
assertThat(senderSignals.items())
|
||||
.extracting(TransferSignalEnvelope::type, TransferSignalEnvelope::payload)
|
||||
.containsExactly(org.assertj.core.groups.Tuple.tuple("answer", "{\"sdp\":\"demo-answer\"}"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user