Files
MeBox/internal/service/strm_115_oauth_test.go
T
truewhile b0fe40142a Rebrand MMTL to MeBox (name, logo, Docker image) (#17)
* Rebrand MMTL to MeBox across codebase and assets

Rename the project display name, Go module path, environment variable
prefix (MEBOX_*), Docker image references, and UI branding from MMTL/mmtl
to MeBox/mebox. Replace logo assets with the new MeBox icon and keep
legacy SQLite migration support for existing mmtl.db deployments.

Co-authored-by: truewhile <truewhile@users.noreply.github.com>

* Fix logo icons: use cube-only crop without truncated text

Previous icon generation cropped too much of the source image, including
partial MeBox wordmark text that was cut off in square icon containers.
Regenerate logo-64/192/512, favicon, and SVG from cube-only region.

Co-authored-by: truewhile <truewhile@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: truewhile <truewhile@users.noreply.github.com>
2026-09-02 16:26:28 +08:00

229 lines
7.8 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 service
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/truewhile/MeBox/internal/model"
"github.com/truewhile/MeBox/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")
}
}