feat(admin): 添加APK管理功能

实现APK文件的上传、下载和列表功能
- 新增APKInfo数据结构
- 添加/admin/apk/upload、/admin/apk/download和/admin/apk/list路由
- 实现APK存储服务方法
- 添加相关测试用例
This commit is contained in:
2026-03-15 18:53:00 +08:00
parent a0ef7f430d
commit e629b69411
6 changed files with 279 additions and 2 deletions

View File

@@ -1,6 +1,7 @@
package handlers_test
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
@@ -324,3 +325,107 @@ func TestAssetsErrorCases(t *testing.T) {
}
}
func doReqWithBody(t *testing.T, h http.Handler, method string, target string, headers map[string]string, body []byte) *httptest.ResponseRecorder {
t.Helper()
req := httptest.NewRequest(method, target, bytes.NewReader(body))
for k, v := range headers {
req.Header.Set(k, v)
}
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
return rec
}
func TestAPKUploadRequiresAuth(t *testing.T) {
h, _ := newTestServer(t)
rec := doReqWithBody(t, h, http.MethodPost, "/admin/apk/upload?runtimeVersion=1.0.0", nil, []byte("fake-apk"))
if rec.Code != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", rec.Code)
}
}
func TestAPKUploadAndDownload(t *testing.T) {
h, updatesRoot := newTestServer(t)
authHeaders := map[string]string{"Authorization": "Bearer dev-token"}
fakeAPK := []byte("PK\x03\x04fake-apk-content-for-testing")
uploadRec := doReqWithBody(t, h, http.MethodPost, "/admin/apk/upload?runtimeVersion=1.0.0", map[string]string{
"Authorization": "Bearer dev-token",
"Content-Type": "application/vnd.android.package-archive",
}, fakeAPK)
if uploadRec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", uploadRec.Code, mustBodyString(t, uploadRec))
}
apkPath := filepath.Join(updatesRoot, "apk", "1.0.0.apk")
if _, err := os.Stat(apkPath); os.IsNotExist(err) {
t.Fatalf("APK file not created at %s", apkPath)
}
downloadRec := doReq(t, h, http.MethodGet, "/admin/apk/download?runtimeVersion=1.0.0", authHeaders)
if downloadRec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", downloadRec.Code)
}
if downloadRec.Header().Get("Content-Type") != "application/vnd.android.package-archive" {
t.Fatalf("expected application/vnd.android.package-archive, got %s", downloadRec.Header().Get("Content-Type"))
}
if downloadRec.Header().Get("Content-Disposition") != "attachment; filename=1.0.0.apk" {
t.Fatalf("unexpected Content-Disposition: %s", downloadRec.Header().Get("Content-Disposition"))
}
body := mustBodyString(t, downloadRec)
if body != string(fakeAPK) {
t.Fatalf("downloaded content mismatch")
}
}
func TestAPKDownloadNotFound(t *testing.T) {
h, _ := newTestServer(t)
authHeaders := map[string]string{"Authorization": "Bearer dev-token"}
rec := doReq(t, h, http.MethodGet, "/admin/apk/download?runtimeVersion=nonexistent", authHeaders)
if rec.Code != http.StatusNotFound {
t.Fatalf("expected 404, got %d", rec.Code)
}
}
func TestAPKList(t *testing.T) {
h, _ := newTestServer(t)
authHeaders := map[string]string{"Authorization": "Bearer dev-token"}
listRec := doReq(t, h, http.MethodGet, "/admin/apk/list", authHeaders)
if listRec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", listRec.Code)
}
body := mustBodyString(t, listRec)
if !strings.Contains(body, `"apks"`) {
t.Fatalf("expected apks array in response: %s", body)
}
fakeAPK := []byte("PK\x03\x04fake-apk-content")
uploadRec := doReqWithBody(t, h, http.MethodPost, "/admin/apk/upload?runtimeVersion=2.0.0", map[string]string{
"Authorization": "Bearer dev-token",
"Content-Type": "application/octet-stream",
}, fakeAPK)
if uploadRec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", uploadRec.Code)
}
listRec2 := doReq(t, h, http.MethodGet, "/admin/apk/list", authHeaders)
if listRec2.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", listRec2.Code)
}
body2 := mustBodyString(t, listRec2)
if !strings.Contains(body2, "2.0.0") {
t.Fatalf("expected 2.0.0 in response: %s", body2)
}
}
func TestAPKUploadRequiresRuntimeVersion(t *testing.T) {
h, _ := newTestServer(t)
rec := doReqWithBody(t, h, http.MethodPost, "/admin/apk/upload", map[string]string{
"Authorization": "Bearer dev-token",
"Content-Type": "application/vnd.android.package-archive",
}, []byte("fake"))
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", rec.Code)
}
}