feat: ship portal and android release updates
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
package com.yoyuzh.config;
|
||||
|
||||
import com.yoyuzh.common.GlobalExceptionHandler;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AndroidReleaseControllerTest {
|
||||
|
||||
@Mock
|
||||
private AndroidReleaseService androidReleaseService;
|
||||
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockMvc = MockMvcBuilders.standaloneSetup(new AndroidReleaseController(androidReleaseService))
|
||||
.setControllerAdvice(new GlobalExceptionHandler())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExposeLatestAndroidReleaseMetadataWithoutAuthentication() throws Exception {
|
||||
AndroidReleaseResponse response = new AndroidReleaseResponse(
|
||||
"https://api.yoyuzh.xyz/api/app/android/download/yoyuzh-portal-2026.04.03.1754.apk",
|
||||
"yoyuzh-portal-2026.04.03.1754.apk",
|
||||
"260931754",
|
||||
"2026.04.03.1754",
|
||||
"2026-04-03T08:33:54Z"
|
||||
);
|
||||
when(androidReleaseService.getLatestRelease()).thenReturn(response);
|
||||
|
||||
mockMvc.perform(get("/api/app/android/latest"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.downloadUrl").value("https://api.yoyuzh.xyz/api/app/android/download/yoyuzh-portal-2026.04.03.1754.apk"))
|
||||
.andExpect(jsonPath("$.data.fileName").value("yoyuzh-portal-2026.04.03.1754.apk"))
|
||||
.andExpect(jsonPath("$.data.versionCode").value("260931754"))
|
||||
.andExpect(jsonPath("$.data.versionName").value("2026.04.03.1754"))
|
||||
.andExpect(jsonPath("$.data.publishedAt").value("2026-04-03T08:33:54Z"));
|
||||
|
||||
verify(androidReleaseService).getLatestRelease();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRedirectAndroidDownloadWithoutAuthentication() throws Exception {
|
||||
when(androidReleaseService.downloadLatestRelease())
|
||||
.thenReturn(new AndroidReleaseDownload("yoyuzh-portal-2026.04.03.1754.apk", "apk-binary".getBytes()));
|
||||
|
||||
mockMvc.perform(get("/api/app/android/download"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, org.hamcrest.Matchers.containsString("filename*=UTF-8''yoyuzh-portal-2026.04.03.1754.apk")));
|
||||
|
||||
verify(androidReleaseService).downloadLatestRelease();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.yoyuzh.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yoyuzh.files.storage.FileContentStorage;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AndroidReleaseServiceTest {
|
||||
|
||||
@Mock
|
||||
private FileContentStorage fileContentStorage;
|
||||
|
||||
private AndroidReleaseProperties properties;
|
||||
private AndroidReleaseService androidReleaseService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
properties = new AndroidReleaseProperties();
|
||||
androidReleaseService = new AndroidReleaseService(fileContentStorage, new ObjectMapper(), properties);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBuildLatestReleaseFromStorageMetadata() {
|
||||
when(fileContentStorage.readBlob("android/releases/latest.json")).thenReturn("""
|
||||
{
|
||||
"objectKey": "android/releases/yoyuzh-portal-2026.04.03.1754.apk",
|
||||
"fileName": "yoyuzh-portal-2026.04.03.1754.apk",
|
||||
"versionCode": "260931754",
|
||||
"versionName": "2026.04.03.1754",
|
||||
"publishedAt": "2026-04-03T09:54:00Z"
|
||||
}
|
||||
""".getBytes());
|
||||
|
||||
AndroidReleaseResponse release = androidReleaseService.getLatestRelease();
|
||||
|
||||
assertEquals("https://api.yoyuzh.xyz/api/app/android/download/yoyuzh-portal-2026.04.03.1754.apk", release.downloadUrl());
|
||||
assertEquals("yoyuzh-portal-2026.04.03.1754.apk", release.fileName());
|
||||
assertEquals("260931754", release.versionCode());
|
||||
assertEquals("2026.04.03.1754", release.versionName());
|
||||
assertEquals("2026-04-03T09:54:00Z", release.publishedAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReadLatestReleaseContentFromStorage() {
|
||||
when(fileContentStorage.readBlob("android/releases/latest.json")).thenReturn("""
|
||||
{
|
||||
"objectKey": "android/releases/yoyuzh-portal-2026.04.03.1754.apk",
|
||||
"fileName": "yoyuzh-portal-2026.04.03.1754.apk"
|
||||
}
|
||||
""".getBytes());
|
||||
when(fileContentStorage.readBlob("android/releases/yoyuzh-portal-2026.04.03.1754.apk"))
|
||||
.thenReturn("apk-binary".getBytes());
|
||||
|
||||
AndroidReleaseDownload download = androidReleaseService.downloadLatestRelease();
|
||||
|
||||
assertEquals("yoyuzh-portal-2026.04.03.1754.apk", download.fileName());
|
||||
assertEquals("apk-binary", new String(download.content()));
|
||||
}
|
||||
}
|
||||
@@ -15,11 +15,16 @@ import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -500,6 +505,75 @@ class FileServiceTest {
|
||||
assertThat(response.url()).isEqualTo("https://download.example.com/file");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseDlUrlForPrivateApkWhenConfigured() {
|
||||
FileStorageProperties properties = new FileStorageProperties();
|
||||
properties.setMaxFileSize(500L * 1024 * 1024);
|
||||
properties.getS3().setPackageDownloadBaseUrl("https://api.yoyuzh.xyz/_dl");
|
||||
properties.getS3().setPackageDownloadSecret("test-secret");
|
||||
properties.getS3().setPackageDownloadTtlSeconds(300);
|
||||
fileService = new FileService(
|
||||
storedFileRepository,
|
||||
fileBlobRepository,
|
||||
fileContentStorage,
|
||||
fileShareLinkRepository,
|
||||
adminMetricsService,
|
||||
properties,
|
||||
Clock.fixed(Instant.parse("2026-04-04T04:30:00Z"), ZoneOffset.UTC)
|
||||
);
|
||||
|
||||
User user = createUser(7L);
|
||||
StoredFile file = createFile(22L, user, "/apps", "安装包.apk");
|
||||
file.setContentType("application/vnd.android.package-archive");
|
||||
when(storedFileRepository.findDetailedById(22L)).thenReturn(Optional.of(file));
|
||||
when(fileContentStorage.supportsDirectDownload()).thenReturn(true);
|
||||
|
||||
DownloadUrlResponse response = fileService.getDownloadUrl(user, 22L);
|
||||
|
||||
URI uri = URI.create(response.url());
|
||||
assertThat(uri.getScheme()).isEqualTo("https");
|
||||
assertThat(uri.getHost()).isEqualTo("api.yoyuzh.xyz");
|
||||
assertThat(uri.getPath()).isEqualTo("/_dl/blobs/blob-22");
|
||||
assertThat(response.url()).contains("expires=1775277300");
|
||||
assertThat(response.url()).contains("md5=1z0AP88pnPz-TpgnYfIT4A");
|
||||
assertThat(response.url()).contains("response-content-disposition=attachment%3B%20filename%3D%22download.apk%22%3B%20filename*%3DUTF-8%27%27%E5%AE%89%E8%A3%85%E5%8C%85.apk");
|
||||
verify(fileContentStorage, never()).createBlobDownloadUrl(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRedirectPrivateApkDownloadToDlWhenConfigured() {
|
||||
FileStorageProperties properties = new FileStorageProperties();
|
||||
properties.setMaxFileSize(500L * 1024 * 1024);
|
||||
properties.getS3().setPackageDownloadBaseUrl("https://api.yoyuzh.xyz/_dl");
|
||||
properties.getS3().setPackageDownloadSecret("test-secret");
|
||||
properties.getS3().setPackageDownloadTtlSeconds(300);
|
||||
fileService = new FileService(
|
||||
storedFileRepository,
|
||||
fileBlobRepository,
|
||||
fileContentStorage,
|
||||
fileShareLinkRepository,
|
||||
adminMetricsService,
|
||||
properties,
|
||||
Clock.fixed(Instant.parse("2026-04-04T04:30:00Z"), ZoneOffset.UTC)
|
||||
);
|
||||
|
||||
User user = createUser(7L);
|
||||
StoredFile file = createFile(22L, user, "/apps", "app-debug.apk");
|
||||
file.setContentType("application/vnd.android.package-archive");
|
||||
when(storedFileRepository.findDetailedById(22L)).thenReturn(Optional.of(file));
|
||||
when(fileContentStorage.supportsDirectDownload()).thenReturn(true);
|
||||
|
||||
ResponseEntity<?> response = fileService.download(user, 22L);
|
||||
|
||||
assertThat(response.getStatusCode().value()).isEqualTo(302);
|
||||
assertThat(response.getHeaders().getLocation()).isNotNull();
|
||||
assertThat(response.getHeaders().getLocation().getHost()).isEqualTo("api.yoyuzh.xyz");
|
||||
assertThat(response.getHeaders().getLocation().getPath()).isEqualTo("/_dl/blobs/blob-22");
|
||||
assertThat(response.getHeaders().getLocation().getQuery()).contains("expires=1775277300");
|
||||
assertThat(response.getHeaders().getLocation().getQuery()).contains("md5=1z0AP88pnPz-TpgnYfIT4A");
|
||||
verify(fileContentStorage, never()).createBlobDownloadUrl(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFallbackToBackendDownloadUrlWhenStorageIsLocal() {
|
||||
User user = createUser(7L);
|
||||
|
||||
Reference in New Issue
Block a user