mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
0aff87c69d
优化
378 lines
11 KiB
Go
378 lines
11 KiB
Go
package cloud115
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// mockTransport 用 httptest server 替换 API 基址。
|
|
func mockAPI(t *testing.T, handler http.HandlerFunc) func() {
|
|
t.Helper()
|
|
pro := httptest.NewServer(handler)
|
|
passport := httptest.NewServer(handler)
|
|
qr := httptest.NewServer(handler)
|
|
oldPro, oldPassport, oldQR := ProAPIBase, PassportAPIBase, QRCodeAPIBase
|
|
ProAPIBase, PassportAPIBase, QRCodeAPIBase = pro.URL, passport.URL, qr.URL
|
|
t.Cleanup(func() {
|
|
ProAPIBase, PassportAPIBase, QRCodeAPIBase = oldPro, oldPassport, oldQR
|
|
pro.Close()
|
|
passport.Close()
|
|
qr.Close()
|
|
})
|
|
return func() {}
|
|
}
|
|
|
|
func TestGetQrCode(t *testing.T) {
|
|
var called bool
|
|
mockAPI(t, func(w http.ResponseWriter, r *http.Request) {
|
|
called = true
|
|
if r.URL.Path != "/open/authDeviceCode" {
|
|
t.Errorf("unexpected path %s", r.URL.Path)
|
|
}
|
|
if err := r.ParseForm(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if r.PostFormValue("client_id") != "100195125" {
|
|
t.Errorf("bad client_id %q", r.PostFormValue("client_id"))
|
|
}
|
|
if r.PostFormValue("code_challenge") == "" {
|
|
t.Errorf("missing code_challenge")
|
|
}
|
|
w.Write([]byte(`{"state":true,"data":{"uid":"U1","time":1700,"sign":"S1","qrcode":"https://img/qr.png"}}`))
|
|
})
|
|
c := NewOpenClient("100195125", "", "")
|
|
qr, err := c.GetQrCode()
|
|
if err != nil {
|
|
t.Fatalf("get qr: %v", err)
|
|
}
|
|
if !called {
|
|
t.Fatal("request not hit")
|
|
}
|
|
if qr.Uid != "U1" || qr.Qrcode == "" || qr.CodeVerifier == "" || len(qr.CodeVerifier) != 64 {
|
|
t.Fatalf("bad qr data: %#v", qr)
|
|
}
|
|
}
|
|
|
|
func TestQrCodeScanStatusSequence(t *testing.T) {
|
|
calls := 0
|
|
mockAPI(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/get/status/" {
|
|
t.Errorf("unexpected path %s", r.URL.Path)
|
|
}
|
|
calls++
|
|
switch calls {
|
|
case 1:
|
|
w.Write([]byte(`{"state":true,"data":{"status":0}}`))
|
|
case 2:
|
|
w.Write([]byte(`{"state":true,"data":{"status":1}}`))
|
|
default:
|
|
w.Write([]byte(`{"state":true,"data":{"status":2}}`))
|
|
}
|
|
})
|
|
c := NewOpenClient("", "", "")
|
|
code := &QrCodeData{Uid: "U1", Time: 1700, Sign: "S1"}
|
|
want := []QrCodeScanStatus{QrCodeScanStatusNotScanned, QrCodeScanStatusScanned, QrCodeScanStatusConfirmed}
|
|
for i, exp := range want {
|
|
got, err := c.QrCodeScanStatus(code)
|
|
if err != nil {
|
|
t.Fatalf("status %d: %v", i, err)
|
|
}
|
|
if got != exp {
|
|
t.Fatalf("status %d: got %v want %v", i, got, exp)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGetTokenAndRefresh(t *testing.T) {
|
|
mockAPI(t, func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/open/deviceCodeToToken":
|
|
w.Write([]byte(`{"state":true,"data":{"access_token":"at1","refresh_token":"rt1","expires_in":7200}}`))
|
|
case "/open/refreshToken":
|
|
if err := r.ParseForm(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if r.PostFormValue("refresh_token") != "rt2" {
|
|
t.Errorf("bad refresh_token %q", r.PostFormValue("refresh_token"))
|
|
}
|
|
w.Write([]byte(`{"state":true,"data":{"access_token":"at2","refresh_token":"rt2","expires_in":7200}}`))
|
|
default:
|
|
t.Errorf("unexpected path %s", r.URL.Path)
|
|
}
|
|
})
|
|
c := NewOpenClient("100195125", "", "")
|
|
token, err := c.GetToken(&QrCodeDataReturn{QrCodeData: QrCodeData{Uid: "U1"}, CodeVerifier: "v"})
|
|
if err != nil {
|
|
t.Fatalf("get token: %v", err)
|
|
}
|
|
if token.AccessToken != "at1" || c.AccessToken != "at1" {
|
|
t.Fatalf("bad token: %#v", token)
|
|
}
|
|
token, err = c.RefreshToken("rt2")
|
|
if err != nil {
|
|
t.Fatalf("refresh: %v", err)
|
|
}
|
|
if token.AccessToken != "at2" || c.RefreshTokenStr != "rt2" {
|
|
t.Fatalf("bad refresh result: %#v", token)
|
|
}
|
|
}
|
|
|
|
func TestRefreshTokenDead(t *testing.T) {
|
|
mockAPI(t, func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte(`{"state":false,"code":40140119,"message":"refresh_token 已过期"}`))
|
|
})
|
|
c := NewOpenClient("100195125", "at", "rt-dead")
|
|
_, err := c.RefreshToken("rt-dead")
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
if !IsRefreshTokenDead(err) {
|
|
t.Fatalf("expected dead refresh token error, got %v", err)
|
|
}
|
|
if c.AccessToken != "" {
|
|
t.Fatalf("dead token should clear access token")
|
|
}
|
|
}
|
|
|
|
func TestFsListAndDownload(t *testing.T) {
|
|
mockAPI(t, func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/open/ufile/files":
|
|
if r.Header.Get("Authorization") != "Bearer at1" {
|
|
t.Errorf("missing auth header")
|
|
}
|
|
w.Write([]byte(`{"state":true,"path":[{"name":"根目录","cid":0}],"data":[
|
|
{"fid":"100","fc":"0","fn":"Movies","fs":0},
|
|
{"fid":"200","fc":"1","fn":"a.mkv","fs":123,"pc":"pickA"}]}`))
|
|
case "/open/ufile/downurl":
|
|
if err := r.ParseForm(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if r.PostFormValue("pick_code") != "pickA" {
|
|
t.Errorf("bad pick_code")
|
|
}
|
|
w.Write([]byte(`{"state":true,"data":{"200":{"file_name":"a.mkv","url":{"url":"https://cdn/x.mkv"}}}}`))
|
|
default:
|
|
t.Errorf("unexpected path %s", r.URL.Path)
|
|
}
|
|
})
|
|
c := NewOpenClient("100195125", "at1", "rt1")
|
|
files, pathStr, err := c.GetFsList(context.Background(), "0", 0, 100)
|
|
if err != nil {
|
|
t.Fatalf("list: %v", err)
|
|
}
|
|
if len(files) != 2 {
|
|
t.Fatalf("want 2 files, got %d", len(files))
|
|
}
|
|
if !files[0].IsDir() {
|
|
t.Fatalf("first entry should be dir")
|
|
}
|
|
if pathStr != "根目录" {
|
|
t.Fatalf("path_str = %q", pathStr)
|
|
}
|
|
url, err := c.GetDownloadURL(context.Background(), "pickA")
|
|
if err != nil {
|
|
t.Fatalf("downurl: %v", err)
|
|
}
|
|
if url != "https://cdn/x.mkv" {
|
|
t.Fatalf("bad url %q", url)
|
|
}
|
|
}
|
|
|
|
func (f RemoteFile) IsDir() bool { return f.Category == TypeDir }
|
|
|
|
func TestGetFsDetailByCid(t *testing.T) {
|
|
mockAPI(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/open/folder/get_info" {
|
|
t.Errorf("unexpected path %s", r.URL.Path)
|
|
}
|
|
w.Write([]byte(`{"state":true,"data":{"file_id":"100","file_name":"Movies","file_category":"0","size_byte":123}}`))
|
|
})
|
|
c := NewOpenClient("100195125", "at1", "rt1")
|
|
detail, err := c.GetFsDetailByCid(context.Background(), "100")
|
|
if err != nil {
|
|
t.Fatalf("detail: %v", err)
|
|
}
|
|
if detail.FileId != "100" || detail.FileName != "Movies" {
|
|
t.Fatalf("bad detail: %#v", detail)
|
|
}
|
|
}
|
|
|
|
// TestRelayRoundTrip 中继加解密往返 + 回调解析。
|
|
func TestRelayRoundTrip(t *testing.T) {
|
|
RelayEncryptionKey = "unit-test-shared-key"
|
|
defer func() { RelayEncryptionKey = "" }()
|
|
|
|
payload := `{"data":{"access_token":"at","refresh_token":"rt","expires_in":7200}}`
|
|
encrypted, err := EncryptRelay(payload)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
decrypted, err := DecryptRelay(encrypted)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if decrypted != payload {
|
|
t.Fatalf("round trip mismatch: %q", decrypted)
|
|
}
|
|
|
|
provider, err := GetOAuthProvider(Source{SourceType: SourceTypeBuiltInRelay, Provider: ProviderQMediaSync, AppID: "QMediaSync"})
|
|
if err != nil {
|
|
t.Fatalf("relay provider should be available when key configured: %v", err)
|
|
}
|
|
token, err := provider.Confirm(context.Background(), map[string]string{"data": encrypted})
|
|
if err != nil {
|
|
t.Fatalf("confirm: %v", err)
|
|
}
|
|
if !token.Done || token.AccessToken != "at" || token.RefreshToken != "rt" {
|
|
t.Fatalf("bad token: %#v", token)
|
|
}
|
|
}
|
|
|
|
func TestRelayProviderRequiresKey(t *testing.T) {
|
|
RelayEncryptionKey = ""
|
|
if _, err := GetOAuthProvider(Source{SourceType: SourceTypeBuiltInRelay, Provider: ProviderQMediaSync, AppID: "QMediaSync"}); err == nil {
|
|
t.Fatal("relay provider should fail without key")
|
|
}
|
|
if RelayAvailable() {
|
|
t.Fatal("RelayAvailable should be false without key")
|
|
}
|
|
}
|
|
|
|
func TestMoviePilotProvider(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/u115/auth_url":
|
|
w.Write([]byte(`{"auth_url":"https://passport.115/auth?state=ST","state":"ST"}`))
|
|
case "/u115/token":
|
|
if r.URL.Query().Get("state") != "ST" {
|
|
t.Errorf("bad state")
|
|
}
|
|
w.Write([]byte(`{"state":true,"data":{"access_token":"at","refresh_token":"rt","expires_in":7200}}`))
|
|
default:
|
|
t.Errorf("unexpected path %s", r.URL.Path)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
provider := moviePilotOAuthProvider{authServer: srv.URL}
|
|
result, err := provider.BuildAuth(context.Background(), OAuthURLRequest{})
|
|
if err != nil {
|
|
t.Fatalf("build: %v", err)
|
|
}
|
|
if result.AuthURL == "" || result.State != "ST" || !result.Polling {
|
|
t.Fatalf("bad result: %#v", result)
|
|
}
|
|
token, err := provider.Poll(context.Background(), "ST")
|
|
if err != nil {
|
|
t.Fatalf("poll: %v", err)
|
|
}
|
|
if !token.Done || token.AccessToken != "at" {
|
|
t.Fatalf("bad token: %#v", token)
|
|
}
|
|
}
|
|
|
|
func TestCloudDriveBuildAuth(t *testing.T) {
|
|
provider := cloudDriveOAuthProvider{source: Source{SourceType: SourceTypeThirdPartyService, Provider: ProviderCloudDrive, AppID: "100195313"}}
|
|
result, err := provider.BuildAuth(context.Background(), OAuthURLRequest{
|
|
RedirectURL: "http://127.0.0.1:8080/api/strm/oauth/callback",
|
|
AuthorizationID: "auth-123",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("build: %v", err)
|
|
}
|
|
if !strings.Contains(result.AuthURL, "client_id=100195313") {
|
|
t.Fatalf("bad auth url: %s", result.AuthURL)
|
|
}
|
|
parsed, err := url.Parse(result.AuthURL)
|
|
if err != nil {
|
|
t.Fatalf("parse auth url: %v", err)
|
|
}
|
|
state, err := url.QueryUnescape(parsed.Query().Get("state"))
|
|
if err != nil {
|
|
t.Fatalf("unescape state: %v", err)
|
|
}
|
|
if !strings.Contains(state, "authorization_id=auth-123") {
|
|
t.Fatalf("missing auth id in state: %s", result.AuthURL)
|
|
}
|
|
token, err := provider.Confirm(context.Background(), map[string]string{
|
|
"access_token": "at", "refresh_token": "rt", "expires_in": "7200",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("confirm: %v", err)
|
|
}
|
|
if !token.Done || token.AccessToken != "at" {
|
|
t.Fatalf("bad token: %#v", token)
|
|
}
|
|
}
|
|
|
|
func TestSourceCatalog(t *testing.T) {
|
|
if len(BuiltInAppIDSources()) < 50 {
|
|
t.Fatalf("built-in app catalog too small: %d", len(BuiltInAppIDSources()))
|
|
}
|
|
if _, ok := FindSource(SourceTypeBuiltInAppID, ProviderOfficialPKCE, "100195125"); !ok {
|
|
t.Fatal("媒体播放器 app not found")
|
|
}
|
|
if len(BuiltInRelaySources()) == 0 || len(ThirdPartySources()) != 2 {
|
|
t.Fatal("relay/thrid-party sources broken")
|
|
}
|
|
}
|
|
|
|
func TestHTTP405AndThrottleRecovery(t *testing.T) {
|
|
tm := NewThrottleManager(100 * time.Millisecond)
|
|
qe := NewQueueExecutor(10, 200, 12000)
|
|
qe.throttleManager = tm
|
|
|
|
mockAPI(t, func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
w.Write([]byte("Method Not Allowed"))
|
|
})
|
|
|
|
c := NewOpenClient("100195125", "at1", "rt1")
|
|
c.executor = qe
|
|
|
|
_, err := c.GetDownloadURL(context.Background(), "pickTest")
|
|
if err == nil {
|
|
t.Fatal("expected 405 error")
|
|
}
|
|
if !tm.IsThrottled() {
|
|
t.Fatal("405 should trigger throttle status")
|
|
}
|
|
|
|
// 验证熔断冷却后能正常恢复
|
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
|
defer cancel()
|
|
if err := tm.WaitThrottleRecovery(ctx); err != nil {
|
|
t.Fatalf("wait throttle recovery failed: %v", err)
|
|
}
|
|
if tm.IsThrottled() {
|
|
t.Fatal("throttle status should be cleared after duration")
|
|
}
|
|
}
|
|
|
|
func TestThrottleCodeHandling(t *testing.T) {
|
|
tm := NewThrottleManager(100 * time.Millisecond)
|
|
qe := NewQueueExecutor(10, 200, 12000)
|
|
qe.throttleManager = tm
|
|
|
|
mockAPI(t, func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte(`{"state":false,"code":770004,"message":"访问频率过高"}`))
|
|
})
|
|
|
|
c := NewOpenClient("100195125", "at1", "rt1")
|
|
c.executor = qe
|
|
|
|
_, _, err := c.GetFsList(context.Background(), "0", 0, 100)
|
|
if err == nil {
|
|
t.Fatal("expected throttle error")
|
|
}
|
|
if !tm.IsThrottled() {
|
|
t.Fatal("code 770004 should trigger throttle status")
|
|
}
|
|
}
|