添加快传7天离线传

This commit is contained in:
yoyuzh
2026-03-24 09:12:10 +08:00
parent e004e64009
commit b9ab1a7640
32 changed files with 1927 additions and 81 deletions

View File

@@ -0,0 +1,37 @@
package com.yoyuzh.config;
import com.yoyuzh.PortalBackendApplication;
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.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest(
classes = PortalBackendApplication.class,
properties = {
"spring.datasource.url=jdbc:h2:mem:api_root_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"
}
)
@AutoConfigureMockMvc
class ApiRootControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
void shouldRedirectRootPathToSwaggerUi() throws Exception {
mockMvc.perform(get("/"))
.andExpect(status().isFound())
.andExpect(redirectedUrl("/swagger-ui.html"));
}
}

View File

@@ -9,6 +9,14 @@ import static org.assertj.core.api.Assertions.assertThat;
class SecurityConfigTest {
@Test
void corsPropertiesShouldAllowProductionSiteOriginsByDefault() {
CorsProperties corsProperties = new CorsProperties();
assertThat(corsProperties.getAllowedOrigins())
.contains("https://yoyuzh.xyz", "https://www.yoyuzh.xyz");
}
@Test
void corsConfigurationShouldAllowPatchRequests() {
CorsProperties corsProperties = new CorsProperties();

View File

@@ -9,14 +9,20 @@ 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.mock.web.MockMultipartFile;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.LocalDateTime;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
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.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest(
@@ -60,8 +66,9 @@ class TransferControllerIntegrationTest {
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"mode": "ONLINE",
"files": [
{"name": "report.pdf", "size": 2048, "contentType": "application/pdf"}
{"name": "report.pdf", "relativePath": "课程资料/report.pdf", "size": 2048, "contentType": "application/pdf"}
]
}
"""))
@@ -69,7 +76,9 @@ class TransferControllerIntegrationTest {
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.sessionId").isNotEmpty())
.andExpect(jsonPath("$.data.pickupCode").isString())
.andExpect(jsonPath("$.data.mode").value("ONLINE"))
.andExpect(jsonPath("$.data.files[0].name").value("report.pdf"))
.andExpect(jsonPath("$.data.files[0].relativePath").value("课程资料/report.pdf"))
.andReturn()
.getResponse()
.getContentAsString();
@@ -80,11 +89,13 @@ class TransferControllerIntegrationTest {
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));
.andExpect(jsonPath("$.data.pickupCode").value(pickupCode))
.andExpect(jsonPath("$.data.mode").value("ONLINE"));
mockMvc.perform(post("/api/transfer/sessions/{sessionId}/join", sessionId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.sessionId").value(sessionId))
.andExpect(jsonPath("$.data.mode").value("ONLINE"))
.andExpect(jsonPath("$.data.files[0].name").value("report.pdf"));
mockMvc.perform(post("/api/transfer/sessions/{sessionId}/signals", sessionId)
@@ -113,11 +124,71 @@ class TransferControllerIntegrationTest {
mockMvc.perform(post("/api/transfer/sessions")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"files":[{"name":"demo.txt","size":12,"contentType":"text/plain"}]}
{"mode":"ONLINE","files":[{"name":"demo.txt","relativePath":"demo.txt","size":12,"contentType":"text/plain"}]}
"""))
.andExpect(status().isUnauthorized());
mockMvc.perform(post("/api/transfer/sessions/{sessionId}/join", "missing-session"))
.andExpect(status().isNotFound());
}
@Test
@WithMockUser(username = "alice")
void shouldPersistOfflineTransfersForSevenDaysAndAllowRepeatedDownloads() throws Exception {
String response = mockMvc.perform(post("/api/transfer/sessions")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"mode": "OFFLINE",
"files": [
{"name": "offline.txt", "relativePath": "资料/offline.txt", "size": 13, "contentType": "text/plain"}
]
}
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.mode").value("OFFLINE"))
.andExpect(jsonPath("$.data.files[0].id").isString())
.andExpect(jsonPath("$.data.files[0].relativePath").value("资料/offline.txt"))
.andReturn()
.getResponse()
.getContentAsString();
String sessionId = com.jayway.jsonpath.JsonPath.read(response, "$.data.sessionId");
String pickupCode = com.jayway.jsonpath.JsonPath.read(response, "$.data.pickupCode");
String fileId = com.jayway.jsonpath.JsonPath.read(response, "$.data.files[0].id");
String expiresAtRaw = com.jayway.jsonpath.JsonPath.read(response, "$.data.expiresAt");
Instant expiresAt = Instant.parse(expiresAtRaw);
assertThat(expiresAt).isAfter(Instant.now().plusSeconds(6 * 24 * 60 * 60L));
MockMultipartFile offlineFile = new MockMultipartFile(
"file",
"offline.txt",
MediaType.TEXT_PLAIN_VALUE,
"hello offline".getBytes(StandardCharsets.UTF_8)
);
mockMvc.perform(multipart("/api/transfer/sessions/{sessionId}/files/{fileId}/content", sessionId, fileId)
.file(offlineFile))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0));
mockMvc.perform(get("/api/transfer/sessions/lookup").param("pickupCode", pickupCode))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.sessionId").value(sessionId))
.andExpect(jsonPath("$.data.mode").value("OFFLINE"));
mockMvc.perform(post("/api/transfer/sessions/{sessionId}/join", sessionId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.mode").value("OFFLINE"))
.andExpect(jsonPath("$.data.files[0].name").value("offline.txt"));
mockMvc.perform(get("/api/transfer/sessions/{sessionId}/files/{fileId}/download", sessionId, fileId))
.andExpect(status().isOk())
.andExpect(content().bytes("hello offline".getBytes(StandardCharsets.UTF_8)));
mockMvc.perform(get("/api/transfer/sessions/{sessionId}/files/{fileId}/download", sessionId, fileId))
.andExpect(status().isOk())
.andExpect(content().bytes("hello offline".getBytes(StandardCharsets.UTF_8)));
}
}

View File

@@ -5,12 +5,13 @@ import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.assertThat;
class TransferSessionTest {
@Test
void shouldEmitPeerJoinedOnlyOnceWhenReceiverJoinsRepeatedly() {
void shouldEmitPeerJoinedOnFirstReceiverJoin() {
TransferSession session = new TransferSession(
"session-1",
"849201",
@@ -18,7 +19,6 @@ class TransferSessionTest {
List.of(new TransferFileItem("report.pdf", 2048, "application/pdf"))
);
session.markReceiverJoined();
session.markReceiverJoined();
PollTransferSignalsResponse senderSignals = session.poll(TransferRole.SENDER, 0);
@@ -29,6 +29,21 @@ class TransferSessionTest {
assertThat(senderSignals.nextCursor()).isEqualTo(1);
}
@Test
void shouldRejectRepeatedReceiverJoinForOnlineTransfer() {
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();
assertThatThrownBy(session::markReceiverJoined)
.hasMessageContaining("在线快传");
}
@Test
void shouldRouteSignalsToTheOppositeRoleQueue() {
TransferSession session = new TransferSession(