添加strm生成

添加strm
This commit is contained in:
truewhile
2026-08-24 14:36:17 +08:00
parent cd1297a30f
commit c55d29ae84
47 changed files with 11956 additions and 4 deletions
+228
View File
@@ -0,0 +1,228 @@
package service
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/ShukeBta/MediaStationGo/internal/model"
"github.com/ShukeBta/MediaStationGo/internal/service/cloud115"
)
// Test115OAuthQRFlow 用 mock 的 115 开放平台接口走完设备码扫码授权:
// 创建账号(空凭据)→ 发起授权(取二维码)→ 轮询(未扫码/已扫码/已确认)
// → 确认后 token 写入账号 → 驱动可用。
func Test115OAuthQRFlow(t *testing.T) {
svc := testStrmService(t)
var scanCalls int
pro := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/open/user/info":
w.Write([]byte(`{"state":true,"data":{"user_id":"115001","user_name":"测试用户"}}`))
default:
t.Errorf("unexpected pro path %s", r.URL.Path)
}
}))
defer pro.Close()
passport := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/open/authDeviceCode":
w.Write([]byte(`{"state":true,"data":{"uid":"U1","time":1700,"sign":"S1","qrcode":"https://img/qr.png"}}`))
case "/open/deviceCodeToToken":
w.Write([]byte(`{"state":true,"data":{"access_token":"open-at","refresh_token":"open-rt","expires_in":7200}}`))
default:
t.Errorf("unexpected passport path %s", r.URL.Path)
}
}))
defer passport.Close()
qr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
scanCalls++
switch scanCalls {
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}}`))
}
}))
defer qr.Close()
oldPro, oldPassport, oldQR := cloud115.ProAPIBase, cloud115.PassportAPIBase, cloud115.QRCodeAPIBase
cloud115.ProAPIBase, cloud115.PassportAPIBase, cloud115.QRCodeAPIBase = pro.URL, passport.URL, qr.URL
defer func() {
cloud115.ProAPIBase, cloud115.PassportAPIBase, cloud115.QRCodeAPIBase = oldPro, oldPassport, oldQR
}()
// 1. 创建 115 账号(自定义 AppID,空凭据)
acct, err := svc.CreateStrmAccount(context.Background(), "我的115", model.StrmProvider115, map[string]string{})
if err != nil {
t.Fatalf("create account: %v", err)
}
if HasStrmAccountCredential(acct) {
t.Fatalf("new account should have no credential")
}
// 2. 发起设备码授权
result, err := svc.Start115OAuth(context.Background(), acct.ID,
string(cloud115.SourceTypeCustomAppID), "100195125", "", "")
if err != nil {
t.Fatalf("start oauth: %v", err)
}
if result.Mode != "qrcode" || result.QRCode == nil || result.QRCode.Qrcode == "" {
t.Fatalf("bad start result: %#v", result)
}
// 3. 轮询:waiting -> scanned
for _, want := range []string{"waiting", "scanned"} {
status, err := svc.Poll115OAuth(context.Background(), result.SessionID)
if err != nil {
t.Fatalf("poll: %v", err)
}
if status.Status != want {
t.Fatalf("poll status = %s, want %s", status.Status, want)
}
}
// 4. 确认
status, err := svc.Poll115OAuth(context.Background(), result.SessionID)
if err != nil {
t.Fatalf("poll confirm: %v", err)
}
if status.Status != "confirmed" {
t.Fatalf("final status = %s, want confirmed", status.Status)
}
// 5. 账号已保存 token 与用户信息
updated, err := svc.repo.StrmAccount.FindByID(context.Background(), acct.ID)
if err != nil || updated == nil {
t.Fatal("account missing")
}
cfg, err := svc.strmAccountConfig(updated)
if err != nil {
t.Fatal(err)
}
if cfg["access_token"] != "open-at" || cfg["refresh_token"] != "open-rt" {
t.Fatalf("token not saved: %#v", cfg)
}
if cfg["user_name"] != "测试用户" && !strings.Contains(updated.Name, "115") && updated.Name != "测试用户" {
t.Fatalf("user info not saved: %#v", cfg)
}
if !HasStrmAccountCredential(updated) {
t.Fatal("credential should be present after auth")
}
}
// Test115OAUTHCallbackRelay 中继回调链路:发起 → 回调(加密 payload)→ 轮询确认。
func Test115OAUTHCallbackRelay(t *testing.T) {
svc := testStrmService(t)
cloud115.RelayEncryptionKey = "callback-test-key"
defer func() { cloud115.RelayEncryptionKey = "" }()
acct, err := svc.CreateStrmAccount(context.Background(), "中继115", model.StrmProvider115, map[string]string{})
if err != nil {
t.Fatal(err)
}
result, err := svc.Start115OAuth(context.Background(), acct.ID,
string(cloud115.SourceTypeBuiltInRelay), "QMediaSync", string(cloud115.ProviderQMediaSync),
"http://127.0.0.1/api/strm/oauth/callback")
if err != nil {
t.Fatalf("start relay oauth: %v", err)
}
if result.Mode != "url" || result.AuthURL == "" {
t.Fatalf("bad relay start: %#v", result)
}
if !strings.Contains(result.AuthURL, "oauth.qmediasync.cn") {
t.Fatalf("unexpected relay url: %s", result.AuthURL)
}
// 模拟中继服务器回调(加密 data + authorization_id)
payload := `{"data":{"access_token":"relay-at","refresh_token":"relay-rt","expires_in":7200}}`
encrypted, err := cloud115.EncryptRelay(payload)
if err != nil {
t.Fatal(err)
}
cbErr := svc.Handle115OAuthCallback(context.Background(), map[string]string{
"authorization_id": result.SessionID,
"data": encrypted,
})
if cbErr != nil {
t.Fatalf("callback: %v", cbErr)
}
// 轮询确认
deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
status, err := svc.Poll115OAuth(context.Background(), result.SessionID)
if err != nil {
t.Fatalf("poll: %v", err)
}
if status.Status == "confirmed" {
break
}
time.Sleep(20 * time.Millisecond)
}
updated, err := svc.repo.StrmAccount.FindByID(context.Background(), acct.ID)
if err != nil || updated == nil {
t.Fatal("account missing")
}
cfg, _ := svc.strmAccountConfig(updated)
if cfg["access_token"] != "relay-at" {
t.Fatalf("relay token not saved: %#v", cfg)
}
}
// Test115TokenRefreshLoop 令牌刷新与失效处理。
func Test115TokenRefreshLoop(t *testing.T) {
svc := testStrmService(t)
var refreshCalls int
passport := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/open/refreshToken" {
t.Errorf("unexpected path %s", r.URL.Path)
}
refreshCalls++
if strings.Contains(r.PostFormValue("refresh_token"), "dead") {
w.Write([]byte(`{"state":false,"code":40140119,"message":"已过期"}`))
return
}
w.Write([]byte(`{"state":true,"data":{"access_token":"new-at","refresh_token":"new-rt","expires_in":7200}}`))
}))
defer passport.Close()
old := cloud115.PassportAPIBase
cloud115.PassportAPIBase = passport.URL
defer func() { cloud115.PassportAPIBase = old }()
cfg := map[string]string{
"app_id": "100195125", "access_token": "at1", "refresh_token": "rt1",
}
acct, err := svc.CreateStrmAccount(context.Background(), "刷新测试", model.StrmProvider115, cfg)
if err != nil {
t.Fatal(err)
}
svc.refresh115TokensOnce(context.Background())
updated, _ := svc.repo.StrmAccount.FindByID(context.Background(), acct.ID)
updatedCfg, _ := svc.strmAccountConfig(updated)
if updatedCfg["access_token"] != "new-at" || updatedCfg["refresh_token"] != "new-rt" {
t.Fatalf("token not refreshed: %#v", updatedCfg)
}
// 失效的 refresh_token:access_token 清空 + 标记失败
deadCfg := map[string]string{"app_id": "100195125", "access_token": "at-d", "refresh_token": "rt-dead"}
deadAcct, err := svc.CreateStrmAccount(context.Background(), "失效测试", model.StrmProvider115, deadCfg)
if err != nil {
t.Fatal(err)
}
svc.refresh115TokensOnce(context.Background())
deadUpdated, _ := svc.repo.StrmAccount.FindByID(context.Background(), deadAcct.ID)
deadUpdatedCfg, _ := svc.strmAccountConfig(deadUpdated)
if deadUpdatedCfg["access_token"] != "" {
t.Fatalf("dead account access_token should be cleared: %#v", deadUpdatedCfg)
}
if deadUpdated.LastTestOK {
t.Fatal("dead account should be marked failed")
}
}