Files
91/backend/internal/proxy/proxy_test.go
T
nianzhibai 7e5e67697e feat: add GuangYaPan drive support
Implement a new GuangYaPan cloud drive integration across the backend, admin UI, playback proxy, and Spider91 migration flow.

Backend changes:\n- Add a GuangYaPan drive driver with token refresh, QR/device login support, directory listing, stream link resolution, directory creation, rename/delete operations, OSS multipart upload, and upload task polling.\n- Register GuangYaPan as a supported storage kind in configuration, catalog normalization, admin APIs, public drive labels, and 302 playback redirects.\n- Allow Spider91 crawler uploads to target GuangYaPan through a dedicated migration adapter.\n- Add scan, thumbnail, preview, and fingerprint cooldown handling for GuangYaPan based on explicit HTTP status codes, Retry-After values, and structured provider codes instead of natural-language message matching.\n- Tighten existing provider cooldown detectors so OneDrive, Google Drive, 115, PikPak, 123pan, Wopan, and media workers avoid treating arbitrary response text as a rate-limit signal.\n- Keep large videos eligible for preview generation unless the user disables preview generation.

Admin and tooling changes:\n- Add GuangYaPan as a selectable drive type with QR login UI and token/root-path credential fields.\n- Add crawler upload target support for GuangYaPan in the admin UI.\n- Add drive branding, labels, metadata display, and docs/config examples for GuangYaPan.\n- Include a standalone GuangYaPan QR login helper script for manual credential acquisition.

Tests:\n- Add GuangYaPan driver, QR login, proxy, admin API, crawler upload target, fingerprint, cooldown, and form coverage.\n- Update rate-limit tests to assert that message-only throttling text no longer starts cooldowns.\n- Cover explicit HTTP status parsing through shared drive helper tests.
2026-06-14 15:44:50 +08:00

360 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package proxy
import (
"context"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"time"
"github.com/video-site/backend/internal/drives"
)
func TestServeStreamRedirectsP115WithRequestUserAgent(t *testing.T) {
reg := NewRegistry()
drv := &proxyFakeDrive{kind: "p115"}
reg.Set("115", drv)
p := New(reg)
req := httptest.NewRequest(http.MethodGet, "/p/stream/115/file-1", nil)
req.Header.Set("User-Agent", "Browser-A")
rr := httptest.NewRecorder()
p.ServeStream(rr, req, "115", "file-1")
if rr.Code != http.StatusFound {
t.Fatalf("status = %d, want %d", rr.Code, http.StatusFound)
}
if got := rr.Header().Get("Location"); got != "https://cdn.example/file-1?ua=Browser-A" {
t.Fatalf("Location = %q", got)
}
if got := drv.calls[0].ua; got != "Browser-A" {
t.Fatalf("link UA = %q, want request UA", got)
}
if got := rr.Header().Get("Referrer-Policy"); got != "no-referrer" {
t.Fatalf("Referrer-Policy = %q", got)
}
}
func TestServeStreamP115CacheIsUserAgentScoped(t *testing.T) {
reg := NewRegistry()
drv := &proxyFakeDrive{kind: "p115"}
reg.Set("115", drv)
p := New(reg)
requestP115(t, p, "115", "file-1", "Browser-A")
requestP115(t, p, "115", "file-1", "Browser-B")
requestP115(t, p, "115", "file-1", "Browser-A")
if len(drv.calls) != 2 {
t.Fatalf("link calls = %d, want 2", len(drv.calls))
}
if drv.calls[0].ua != "Browser-A" || drv.calls[1].ua != "Browser-B" {
t.Fatalf("link UAs = %#v", drv.calls)
}
}
func requestP115(t *testing.T, p *Proxy, driveID, fileID, ua string) {
t.Helper()
req := httptest.NewRequest(http.MethodGet, "/p/stream/"+driveID+"/"+fileID, nil)
req.Header.Set("User-Agent", ua)
rr := httptest.NewRecorder()
p.ServeStream(rr, req, driveID, fileID)
if rr.Code != http.StatusFound {
t.Fatalf("status = %d, want %d", rr.Code, http.StatusFound)
}
}
type proxyFakeDrive struct {
kind string
calls []proxyFakeCall
}
type proxyFakeCall struct {
fileID string
ua string
}
func (d *proxyFakeDrive) Kind() string { return d.kind }
func (d *proxyFakeDrive) ID() string { return "fake" }
func (d *proxyFakeDrive) Init(context.Context) error {
return nil
}
func (d *proxyFakeDrive) List(context.Context, string) ([]drives.Entry, error) {
return nil, drives.ErrNotSupported
}
func (d *proxyFakeDrive) Stat(context.Context, string) (*drives.Entry, error) {
return nil, drives.ErrNotSupported
}
func (d *proxyFakeDrive) StreamURL(ctx context.Context, fileID string) (*drives.StreamLink, error) {
return d.StreamURLWithHeader(ctx, fileID, nil)
}
func (d *proxyFakeDrive) StreamURLWithHeader(_ context.Context, fileID string, header http.Header) (*drives.StreamLink, error) {
ua := header.Get("User-Agent")
d.calls = append(d.calls, proxyFakeCall{fileID: fileID, ua: ua})
return &drives.StreamLink{
URL: "https://cdn.example/" + fileID + "?ua=" + ua,
Headers: http.Header{"User-Agent": {ua}},
Expires: time.Now().Add(time.Minute),
}, nil
}
func (d *proxyFakeDrive) Upload(context.Context, string, string, io.Reader, int64) (string, error) {
return "", drives.ErrNotSupported
}
func (d *proxyFakeDrive) EnsureDir(context.Context, string) (string, error) {
return "", drives.ErrNotSupported
}
func (d *proxyFakeDrive) RootID() string { return "0" }
func TestServeStreamRedirectsPikPakWithoutUserAgentScopedCache(t *testing.T) {
reg := NewRegistry()
drv := &proxyFakePikPakDrive{}
reg.Set("pikpak", drv)
p := New(reg)
// 三次请求,两个不同 UA。pikpak 取链不依赖 UA,所以缓存 key 只看 driveID/fileID
// 30 秒内只会真正调用 driver 一次。
requestPikPak(t, p, "pikpak", "file-1", "Browser-A")
requestPikPak(t, p, "pikpak", "file-1", "Browser-B")
requestPikPak(t, p, "pikpak", "file-1", "Browser-A")
if drv.calls != 1 {
t.Fatalf("link calls = %d, want 1 (cache must not be UA-scoped for pikpak)", drv.calls)
}
}
func TestServeStreamPikPakSetsRedirectHeaders(t *testing.T) {
reg := NewRegistry()
drv := &proxyFakePikPakDrive{}
reg.Set("pikpak", drv)
p := New(reg)
req := httptest.NewRequest(http.MethodGet, "/p/stream/pikpak/file-1", nil)
req.Header.Set("User-Agent", "Browser-A")
rr := httptest.NewRecorder()
p.ServeStream(rr, req, "pikpak", "file-1")
if rr.Code != http.StatusFound {
t.Fatalf("status = %d, want %d", rr.Code, http.StatusFound)
}
if got := rr.Header().Get("Location"); got != "https://cdn.pikpak.example/file-1" {
t.Fatalf("Location = %q", got)
}
if got := rr.Header().Get("Referrer-Policy"); got != "no-referrer" {
t.Fatalf("Referrer-Policy = %q", got)
}
}
func TestServeStreamRedirectsOneDrive(t *testing.T) {
reg := NewRegistry()
drv := &proxyFakeSimpleDrive{
kind: "onedrive",
url: "https://public.onedrive.example/video.mp4",
}
reg.Set("onedrive", drv)
p := New(reg)
req := httptest.NewRequest(http.MethodGet, "/p/stream/onedrive/file-1", nil)
rr := httptest.NewRecorder()
p.ServeStream(rr, req, "onedrive", "file-1")
if rr.Code != http.StatusFound {
t.Fatalf("status = %d, want %d", rr.Code, http.StatusFound)
}
if got := rr.Header().Get("Location"); got != "https://public.onedrive.example/video.mp4" {
t.Fatalf("Location = %q", got)
}
if drv.calls != 1 {
t.Fatalf("link calls = %d, want 1", drv.calls)
}
}
func TestServeStreamRedirectsP123(t *testing.T) {
reg := NewRegistry()
drv := &proxyFakeSimpleDrive{
kind: "p123",
url: "https://cdn.123pan.example/video.mp4",
}
reg.Set("p123", drv)
p := New(reg)
req := httptest.NewRequest(http.MethodGet, "/p/stream/p123/file-1", nil)
rr := httptest.NewRecorder()
p.ServeStream(rr, req, "p123", "file-1")
if rr.Code != http.StatusFound {
t.Fatalf("status = %d, want %d", rr.Code, http.StatusFound)
}
if got := rr.Header().Get("Location"); got != "https://cdn.123pan.example/video.mp4" {
t.Fatalf("Location = %q", got)
}
if drv.calls != 1 {
t.Fatalf("link calls = %d, want 1", drv.calls)
}
}
func TestServeStreamRedirectsWopan(t *testing.T) {
reg := NewRegistry()
drv := &proxyFakeSimpleDrive{
kind: "wopan",
url: "https://du.smartont.net:8445/openapi/download?fid=encoded",
}
reg.Set("wopan", drv)
p := New(reg)
req := httptest.NewRequest(http.MethodGet, "/p/stream/wopan/file-1", nil)
rr := httptest.NewRecorder()
p.ServeStream(rr, req, "wopan", "file-1")
if rr.Code != http.StatusFound {
t.Fatalf("status = %d, want %d", rr.Code, http.StatusFound)
}
if got := rr.Header().Get("Location"); got != "https://du.smartont.net:8445/openapi/download?fid=encoded" {
t.Fatalf("Location = %q", got)
}
if drv.calls != 1 {
t.Fatalf("link calls = %d, want 1", drv.calls)
}
}
func TestServeStreamRedirectsGuangYaPan(t *testing.T) {
reg := NewRegistry()
drv := &proxyFakeSimpleDrive{
kind: "guangyapan",
url: "https://cdn.guangyapan.example/video.mp4?sign=encoded",
}
reg.Set("guangyapan", drv)
p := New(reg)
req := httptest.NewRequest(http.MethodGet, "/p/stream/guangyapan/file-1", nil)
rr := httptest.NewRecorder()
p.ServeStream(rr, req, "guangyapan", "file-1")
if rr.Code != http.StatusFound {
t.Fatalf("status = %d, want %d", rr.Code, http.StatusFound)
}
if got := rr.Header().Get("Location"); got != "https://cdn.guangyapan.example/video.mp4?sign=encoded" {
t.Fatalf("Location = %q", got)
}
if drv.calls != 1 {
t.Fatalf("link calls = %d, want 1", drv.calls)
}
}
func TestServeStreamServesLocalFilePath(t *testing.T) {
path := filepath.Join(t.TempDir(), "video.mp4")
if err := os.WriteFile(path, []byte("0123456789"), 0o644); err != nil {
t.Fatalf("write local file: %v", err)
}
reg := NewRegistry()
drv := &proxyFakeSimpleDrive{
kind: "localstorage",
url: path,
}
reg.Set("local", drv)
p := New(reg)
req := httptest.NewRequest(http.MethodGet, "/p/stream/local/file-1", nil)
req.Header.Set("Range", "bytes=2-5")
rr := httptest.NewRecorder()
p.ServeStream(rr, req, "local", "file-1")
if rr.Code != http.StatusPartialContent {
t.Fatalf("status = %d, want %d", rr.Code, http.StatusPartialContent)
}
if got := rr.Body.String(); got != "2345" {
t.Fatalf("body = %q, want range bytes", got)
}
if drv.calls != 1 {
t.Fatalf("link calls = %d, want 1", drv.calls)
}
}
func requestPikPak(t *testing.T, p *Proxy, driveID, fileID, ua string) {
t.Helper()
req := httptest.NewRequest(http.MethodGet, "/p/stream/"+driveID+"/"+fileID, nil)
req.Header.Set("User-Agent", ua)
rr := httptest.NewRecorder()
p.ServeStream(rr, req, driveID, fileID)
if rr.Code != http.StatusFound {
t.Fatalf("status = %d, want %d", rr.Code, http.StatusFound)
}
}
// proxyFakePikPakDrive 故意不实现 streamURLWithHeader
// 用来回归 pikpak 取链不带 UA 作用域、且走 302 的不变量。
type proxyFakePikPakDrive struct {
calls int
}
func (d *proxyFakePikPakDrive) Kind() string { return "pikpak" }
func (d *proxyFakePikPakDrive) ID() string { return "pikpak" }
func (d *proxyFakePikPakDrive) Init(context.Context) error {
return nil
}
func (d *proxyFakePikPakDrive) List(context.Context, string) ([]drives.Entry, error) {
return nil, drives.ErrNotSupported
}
func (d *proxyFakePikPakDrive) Stat(context.Context, string) (*drives.Entry, error) {
return nil, drives.ErrNotSupported
}
func (d *proxyFakePikPakDrive) StreamURL(_ context.Context, fileID string) (*drives.StreamLink, error) {
d.calls++
return &drives.StreamLink{
URL: "https://cdn.pikpak.example/" + fileID,
Headers: http.Header{},
Expires: time.Now().Add(10 * time.Minute),
}, nil
}
func (d *proxyFakePikPakDrive) Upload(context.Context, string, string, io.Reader, int64) (string, error) {
return "", drives.ErrNotSupported
}
func (d *proxyFakePikPakDrive) EnsureDir(context.Context, string) (string, error) {
return "", drives.ErrNotSupported
}
func (d *proxyFakePikPakDrive) RootID() string { return "0" }
type proxyFakeSimpleDrive struct {
kind string
url string
calls int
}
func (d *proxyFakeSimpleDrive) Kind() string { return d.kind }
func (d *proxyFakeSimpleDrive) ID() string { return d.kind }
func (d *proxyFakeSimpleDrive) Init(context.Context) error {
return nil
}
func (d *proxyFakeSimpleDrive) List(context.Context, string) ([]drives.Entry, error) {
return nil, drives.ErrNotSupported
}
func (d *proxyFakeSimpleDrive) Stat(context.Context, string) (*drives.Entry, error) {
return nil, drives.ErrNotSupported
}
func (d *proxyFakeSimpleDrive) StreamURL(context.Context, string) (*drives.StreamLink, error) {
d.calls++
return &drives.StreamLink{
URL: d.url,
Headers: http.Header{},
Expires: time.Now().Add(10 * time.Minute),
}, nil
}
func (d *proxyFakeSimpleDrive) Upload(context.Context, string, string, io.Reader, int64) (string, error) {
return "", drives.ErrNotSupported
}
func (d *proxyFakeSimpleDrive) EnsureDir(context.Context, string) (string, error) {
return "", drives.ErrNotSupported
}
func (d *proxyFakeSimpleDrive) RootID() string { return "0" }