mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-02 20:26:36 +08:00
175 lines
5.9 KiB
Go
175 lines
5.9 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// TestReaderLocalAudioStream 本地有声书音频流端点:
|
|
// 签名鉴权 + Range 透传(播放器拖进度靠它)。
|
|
func TestReaderLocalAudioStream(t *testing.T) {
|
|
container := newReaderHandlerContainer(t)
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
router.GET("/api/reader/local/audio", readerLocalAudioHandler(container))
|
|
router.HEAD("/api/reader/local/audio", readerLocalAudioHandler(container))
|
|
|
|
payload := []byte("0123456789abcdefghij")
|
|
path := filepath.Join(t.TempDir(), "track.mp3")
|
|
if err := os.WriteFile(path, payload, 0o640); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
signed := container.Reader.LocalAudioURL("book-1", path)
|
|
|
|
// 全量拉取
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, httptest.NewRequest(http.MethodGet, signed, nil))
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("全量拉取 status=%d body=%s", w.Code, w.Body.String())
|
|
}
|
|
if w.Body.String() != string(payload) {
|
|
t.Fatalf("全量拉取 body=%q", w.Body.String())
|
|
}
|
|
|
|
// Range 请求要能拖进度
|
|
w = httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, signed, nil)
|
|
req.Header.Set("Range", "bytes=2-5")
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != http.StatusPartialContent {
|
|
t.Fatalf("Range 请求 status=%d", w.Code)
|
|
}
|
|
if w.Body.String() != "2345" {
|
|
t.Fatalf("Range 请求 body=%q", w.Body.String())
|
|
}
|
|
|
|
// 篡改签名应被拒绝
|
|
w = httptest.NewRecorder()
|
|
router.ServeHTTP(w, httptest.NewRequest(http.MethodGet, signed+"x", nil))
|
|
if w.Code != http.StatusForbidden {
|
|
t.Fatalf("坏签名 status=%d,期望 403", w.Code)
|
|
}
|
|
|
|
// 文件不存在 → 404(而不是 500)
|
|
missing := container.Reader.LocalAudioURL("book-1", filepath.Join(t.TempDir(), "nope.mp3"))
|
|
w = httptest.NewRecorder()
|
|
router.ServeHTTP(w, httptest.NewRequest(http.MethodGet, missing, nil))
|
|
if w.Code != http.StatusNotFound {
|
|
t.Fatalf("缺失文件 status=%d,期望 404", w.Code)
|
|
}
|
|
}
|
|
|
|
// TestReaderAudioTranscodeRoute 转码端点:签名鉴权,命中缓存后按 Range 下发。
|
|
// 用预置的缓存文件代替真实转码,避免测试依赖 ffmpeg。
|
|
func TestReaderAudioTranscodeRoute(t *testing.T) {
|
|
container := newReaderHandlerContainer(t)
|
|
container.Cfg.App.DataDir = t.TempDir() // 转码缓存落在临时目录,别写进仓库
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
router.GET("/api/reader/audio/transcode", readerAudioTranscodeHandler(container))
|
|
router.HEAD("/api/reader/audio/transcode", readerAudioTranscodeHandler(container))
|
|
|
|
const bookID = "book-1"
|
|
source := `D:\media\斗破苍穹\001.wma`
|
|
payload := []byte("ID3FAKEMP3PAYLOAD0123456789")
|
|
|
|
cachePath, err := container.Reader.AudioTranscodeCachePath(bookID, source)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(cachePath), 0o750); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(cachePath, payload, 0o640); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
signed := container.Reader.AudioTranscodeURL(bookID, source)
|
|
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, httptest.NewRequest(http.MethodGet, signed, nil))
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("全量拉取 status=%d body=%s", w.Code, w.Body.String())
|
|
}
|
|
if w.Body.String() != string(payload) {
|
|
t.Fatalf("全量拉取 body=%q", w.Body.String())
|
|
}
|
|
|
|
// 拖进度靠 Range
|
|
w = httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, signed, nil)
|
|
req.Header.Set("Range", "bytes=3-7")
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != http.StatusPartialContent {
|
|
t.Fatalf("Range 请求 status=%d", w.Code)
|
|
}
|
|
if w.Body.String() != "FAKEM" {
|
|
t.Fatalf("Range 请求 body=%q", w.Body.String())
|
|
}
|
|
|
|
// 篡改签名应被拒绝
|
|
w = httptest.NewRecorder()
|
|
router.ServeHTTP(w, httptest.NewRequest(http.MethodGet, signed+"x", nil))
|
|
if w.Code != http.StatusForbidden {
|
|
t.Fatalf("坏签名 status=%d,期望 403", w.Code)
|
|
}
|
|
|
|
// 换一本书(签名不匹配)同样拒绝
|
|
forged := strings.Replace(signed, "b=book-1", "b=book-2", 1)
|
|
w = httptest.NewRecorder()
|
|
router.ServeHTTP(w, httptest.NewRequest(http.MethodGet, forged, nil))
|
|
if w.Code != http.StatusForbidden {
|
|
t.Fatalf("换书后 status=%d,期望 403", w.Code)
|
|
}
|
|
}
|
|
|
|
// TestReaderAudioTranscodeRouteWithoutFFmpeg 没装 ffmpeg 且无缓存时返回明确错误,
|
|
// 而不是空响应(前端据此提示用户去装 ffmpeg)。
|
|
func TestReaderAudioTranscodeRouteWithoutFFmpeg(t *testing.T) {
|
|
container := newReaderHandlerContainer(t)
|
|
container.Cfg.App.DataDir = t.TempDir()
|
|
container.Cfg.App.FFmpegPath = filepath.Join(t.TempDir(), "definitely-missing-ffmpeg")
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
router.GET("/api/reader/audio/transcode", readerAudioTranscodeHandler(container))
|
|
|
|
signed := container.Reader.AudioTranscodeURL("book-1", `D:\media\001.wma`)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, httptest.NewRequest(http.MethodGet, signed, nil))
|
|
if w.Code != http.StatusBadGateway {
|
|
t.Fatalf("status=%d,期望 502", w.Code)
|
|
}
|
|
if !strings.Contains(w.Body.String(), "ffmpeg") {
|
|
t.Fatalf("错误正文应提到 ffmpeg: %q", w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestReaderLocalAudioRejectsOtherBook 换一本书的签名不通用:防止拿到别人的音频地址。
|
|
func TestReaderLocalAudioRejectsOtherBook(t *testing.T) {
|
|
container := newReaderHandlerContainer(t)
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
router.GET("/api/reader/local/audio", readerLocalAudioHandler(container))
|
|
|
|
path := filepath.Join(t.TempDir(), "track.mp3")
|
|
if err := os.WriteFile(path, []byte("x"), 0o640); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
signed := container.Reader.LocalAudioURL("book-1", path)
|
|
forged := strings.Replace(signed, "b=book-1", "b=book-2", 1)
|
|
if forged == signed {
|
|
t.Fatal("测试用例未改写 book 参数")
|
|
}
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, httptest.NewRequest(http.MethodGet, forged, nil))
|
|
if w.Code != http.StatusForbidden {
|
|
t.Fatalf("换书后签名应失效,status=%d", w.Code)
|
|
}
|
|
}
|