mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 03:06:38 +08:00
348 lines
10 KiB
Go
348 lines
10 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/truewhile/MeBox/internal/config"
|
|
"github.com/truewhile/MeBox/internal/model"
|
|
"github.com/truewhile/MeBox/internal/repository"
|
|
"github.com/truewhile/MeBox/internal/service/cloud"
|
|
"github.com/truewhile/MeBox/internal/service/cloud115"
|
|
)
|
|
|
|
func TestStrmAccountConfigPreviewOf(t *testing.T) {
|
|
svc := testStrmService(t)
|
|
acct := &model.StrmAccount{
|
|
Provider: model.StrmProviderOpenList,
|
|
Config: mustJSON(map[string]string{
|
|
"server": "https://list.example.com",
|
|
"username": "alice",
|
|
"password": svc.crypto.Encrypt("secret"),
|
|
"token": svc.crypto.Encrypt("tok"),
|
|
}),
|
|
}
|
|
|
|
preview := svc.StrmAccountConfigPreviewOf(acct)
|
|
if preview.Server != "https://list.example.com" {
|
|
t.Fatalf("server = %q", preview.Server)
|
|
}
|
|
if preview.Username != "alice" {
|
|
t.Fatalf("username = %q", preview.Username)
|
|
}
|
|
if !preview.HasPassword || !preview.HasToken {
|
|
t.Fatalf("expected secret flags, got %#v", preview)
|
|
}
|
|
}
|
|
|
|
func TestUpdateStrmAccountMergesConfigWithoutClearingSecrets(t *testing.T) {
|
|
svc := testStrmService(t)
|
|
ctx := context.Background()
|
|
acct, err := svc.CreateStrmAccount(ctx, "openlist", model.StrmProviderOpenList, map[string]string{
|
|
"server": "https://list.example.com",
|
|
"username": "alice",
|
|
"password": "secret",
|
|
"token": "tok",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create account: %v", err)
|
|
}
|
|
|
|
updated, err := svc.UpdateStrmAccount(ctx, acct.ID, "openlist-renamed", nil, map[string]string{
|
|
"server": "https://list.example.com",
|
|
"username": "alice",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("update account: %v", err)
|
|
}
|
|
if updated.Name != "openlist-renamed" {
|
|
t.Fatalf("name = %q", updated.Name)
|
|
}
|
|
cfg, err := svc.strmAccountConfig(updated)
|
|
if err != nil {
|
|
t.Fatalf("decode config: %v", err)
|
|
}
|
|
if cfg["password"] != "secret" {
|
|
t.Fatalf("password was cleared: %#v", cfg)
|
|
}
|
|
if cfg["token"] != "tok" {
|
|
t.Fatalf("token was cleared: %#v", cfg)
|
|
}
|
|
}
|
|
|
|
func TestProviderForReuses115ClientUntilCredentialsChange(t *testing.T) {
|
|
svc := testStrmService(t)
|
|
ctx := context.Background()
|
|
acct, err := svc.CreateStrmAccount(ctx, "115", model.StrmProvider115, map[string]string{
|
|
"app_id": "100195129",
|
|
"access_token": "at-1",
|
|
"refresh_token": "rt-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create account: %v", err)
|
|
}
|
|
|
|
first, err := svc.providerFor(ctx, acct)
|
|
if err != nil {
|
|
t.Fatalf("first provider: %v", err)
|
|
}
|
|
second, err := svc.providerFor(ctx, acct)
|
|
if err != nil {
|
|
t.Fatalf("second provider: %v", err)
|
|
}
|
|
if first != second {
|
|
t.Fatal("115 provider should be shared per account")
|
|
}
|
|
|
|
updated, err := svc.UpdateStrmAccount(ctx, acct.ID, "", nil, map[string]string{
|
|
"access_token": "at-2",
|
|
"refresh_token": "rt-2",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("update account: %v", err)
|
|
}
|
|
third, err := svc.providerFor(ctx, updated)
|
|
if err != nil {
|
|
t.Fatalf("provider after credential update: %v", err)
|
|
}
|
|
if first == third {
|
|
t.Fatal("credential update must invalidate the shared provider")
|
|
}
|
|
|
|
oldClient := first.(interface{ OpenClient() *cloud115.OpenClient }).OpenClient()
|
|
oldClient.OnTokenRefreshed("at-stale", "rt-stale")
|
|
fresh, err := svc.repo.StrmAccount.FindByID(ctx, acct.ID)
|
|
if err != nil || fresh == nil {
|
|
t.Fatalf("reload account: %v", err)
|
|
}
|
|
freshCfg, err := svc.strmAccountConfig(fresh)
|
|
if err != nil {
|
|
t.Fatalf("decode refreshed account: %v", err)
|
|
}
|
|
if freshCfg["access_token"] != "at-2" || freshCfg["refresh_token"] != "rt-2" {
|
|
t.Fatalf("stale client overwrote new credentials: %#v", freshCfg)
|
|
}
|
|
}
|
|
|
|
func TestPersist115TokensKeepsSharedClient(t *testing.T) {
|
|
svc := testStrmService(t)
|
|
ctx := context.Background()
|
|
acct, err := svc.CreateStrmAccount(ctx, "115", model.StrmProvider115, map[string]string{
|
|
"app_id": "100195129",
|
|
"access_token": "at-1",
|
|
"refresh_token": "rt-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create account: %v", err)
|
|
}
|
|
first, err := svc.providerFor(ctx, acct)
|
|
if err != nil {
|
|
t.Fatalf("first provider: %v", err)
|
|
}
|
|
|
|
svc.persist115Tokens(acct.ID, "at-2", "rt-2")
|
|
fresh, err := svc.repo.StrmAccount.FindByID(ctx, acct.ID)
|
|
if err != nil || fresh == nil {
|
|
t.Fatalf("reload account: %v", err)
|
|
}
|
|
second, err := svc.providerFor(ctx, fresh)
|
|
if err != nil {
|
|
t.Fatalf("second provider: %v", err)
|
|
}
|
|
if first != second {
|
|
t.Fatal("automatic token persistence must keep the in-memory shared client")
|
|
}
|
|
}
|
|
|
|
// TestTestStrmAccountKeepsTokensRefreshedDuringPing 回归测试:
|
|
// Ping 期间 115 客户端可能刷新并持久化 token。账号测试只能写 last_test_*
|
|
// 字段,不能再用请求开始时读取的旧 Config 整包覆盖,否则新 token 会被旧值
|
|
// 覆盖,最终导致账号在下次重启后失效。
|
|
func TestTestStrmAccountKeepsTokensRefreshedDuringPing(t *testing.T) {
|
|
svc := testStrmService(t)
|
|
ctx := context.Background()
|
|
acct, err := svc.CreateStrmAccount(ctx, "115", model.StrmProvider115, map[string]string{
|
|
"app_id": "100195129",
|
|
"access_token": "at-old",
|
|
"refresh_token": "rt-old",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create account: %v", err)
|
|
}
|
|
|
|
svc.providerMu.Lock()
|
|
svc.provider115Cache[acct.ID] = &refreshOnPing115Provider{svc: svc, accountID: acct.ID}
|
|
svc.providerMu.Unlock()
|
|
|
|
got := svc.TestStrmAccount(ctx, acct.ID)
|
|
if got == nil {
|
|
t.Fatal("TestStrmAccount returned nil")
|
|
}
|
|
if !got.LastTestOK || got.LastTestResult != "ok" {
|
|
t.Fatalf("test result = (%v, %q), want ok", got.LastTestOK, got.LastTestResult)
|
|
}
|
|
gotCfg, err := svc.strmAccountConfig(got)
|
|
if err != nil {
|
|
t.Fatalf("decode returned config: %v", err)
|
|
}
|
|
if gotCfg["access_token"] != "at-new" || gotCfg["refresh_token"] != "rt-new" {
|
|
t.Fatalf("returned account lost refreshed tokens: %#v", gotCfg)
|
|
}
|
|
|
|
fresh, err := svc.repo.StrmAccount.FindByID(ctx, acct.ID)
|
|
if err != nil || fresh == nil {
|
|
t.Fatalf("reload account: %v", err)
|
|
}
|
|
freshCfg, err := svc.strmAccountConfig(fresh)
|
|
if err != nil {
|
|
t.Fatalf("decode persisted config: %v", err)
|
|
}
|
|
if freshCfg["access_token"] != "at-new" || freshCfg["refresh_token"] != "rt-new" {
|
|
t.Fatalf("persisted account lost refreshed tokens: %#v", freshCfg)
|
|
}
|
|
}
|
|
|
|
// refreshOnPing115Provider 模拟真实 115 客户端在 Ping 内完成 token 轮转并
|
|
// 通过持久化回调写回新 token 的行为。
|
|
type refreshOnPing115Provider struct {
|
|
svc *StrmService
|
|
accountID string
|
|
}
|
|
|
|
func (p *refreshOnPing115Provider) Type() string { return model.StrmProvider115 }
|
|
|
|
func (p *refreshOnPing115Provider) Ping(context.Context) error {
|
|
p.svc.persist115Tokens(p.accountID, "at-new", "rt-new")
|
|
return nil
|
|
}
|
|
|
|
func (p *refreshOnPing115Provider) List(context.Context, string) ([]cloud.FileEntry, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (p *refreshOnPing115Provider) Resolve(context.Context, string) (*cloud.DirectLink, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func TestDeleteStrmAccountCascadesEmbyMounts(t *testing.T) {
|
|
ctx := context.Background()
|
|
db := newServiceTestDB(t, &model.StrmAccount{}, &model.EmbyMount{}, &model.StrmSyncPath{})
|
|
repos := repository.New(db)
|
|
svc := NewStrmService(nil, zap.NewNop(), repos, nil)
|
|
|
|
createEmbyAcct := func(id, name string) *model.StrmAccount {
|
|
acct := &model.StrmAccount{
|
|
Base: model.Base{ID: id},
|
|
Name: name,
|
|
Provider: model.StrmProviderEmbyRemote,
|
|
Enabled: true,
|
|
}
|
|
if err := repos.StrmAccount.Create(ctx, acct); err != nil {
|
|
t.Fatalf("create account %s: %v", id, err)
|
|
}
|
|
return acct
|
|
}
|
|
createMounts := func(accountID string, viewIDs ...string) {
|
|
for _, vid := range viewIDs {
|
|
m := &model.EmbyMount{
|
|
AccountID: accountID,
|
|
RemoteViewID: vid,
|
|
RemoteViewName: "库-" + vid,
|
|
Enabled: true,
|
|
}
|
|
if err := repos.EmbyMount.Create(ctx, m); err != nil {
|
|
t.Fatalf("create mount %s: %v", vid, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
gone := createEmbyAcct("acct-gone", "要删除的账号")
|
|
createMounts(gone.ID, "view-1", "view-2", "view-3")
|
|
keep := createEmbyAcct("acct-keep", "保留的账号")
|
|
createMounts(keep.ID, "view-a")
|
|
|
|
if err := svc.DeleteStrmAccount(ctx, gone.ID); err != nil {
|
|
t.Fatalf("DeleteStrmAccount failed: %v", err)
|
|
}
|
|
|
|
remaining, err := repos.StrmAccount.FindByID(ctx, gone.ID)
|
|
if err != nil {
|
|
t.Fatalf("find account: %v", err)
|
|
}
|
|
if remaining != nil {
|
|
t.Fatalf("account %s should be deleted", gone.ID)
|
|
}
|
|
goneMounts, err := repos.EmbyMount.ListByAccountID(ctx, gone.ID)
|
|
if err != nil {
|
|
t.Fatalf("list mounts: %v", err)
|
|
}
|
|
if len(goneMounts) != 0 {
|
|
t.Fatalf("deleted account still has %d mounts (orphans)", len(goneMounts))
|
|
}
|
|
keepMounts, err := repos.EmbyMount.ListByAccountID(ctx, keep.ID)
|
|
if err != nil {
|
|
t.Fatalf("list mounts: %v", err)
|
|
}
|
|
if len(keepMounts) != 1 || keepMounts[0].RemoteViewID != "view-a" {
|
|
t.Fatalf("keep account mounts = %#v, want 1 (view-a)", keepMounts)
|
|
}
|
|
}
|
|
|
|
func TestCleanupOrphanMountsRemovesStaleRows(t *testing.T) {
|
|
ctx := context.Background()
|
|
db := newServiceTestDB(t, &model.StrmAccount{}, &model.EmbyMount{})
|
|
repos := repository.New(db)
|
|
svc := NewEmbyRemoteService(&config.Config{}, zap.NewNop(), repos, nil)
|
|
|
|
acct := &model.StrmAccount{
|
|
Base: model.Base{ID: "acct-1"},
|
|
Name: "emby",
|
|
Provider: model.StrmProviderEmbyRemote,
|
|
Enabled: true,
|
|
}
|
|
if err := repos.StrmAccount.Create(ctx, acct); err != nil {
|
|
t.Fatalf("create account: %v", err)
|
|
}
|
|
for i, vid := range []string{"v1", "v2", "v-orphan-1", "v-orphan-2"} {
|
|
m := &model.EmbyMount{
|
|
Base: model.Base{ID: "mount-" + vid},
|
|
AccountID: acct.ID,
|
|
RemoteViewID: vid,
|
|
Enabled: true,
|
|
}
|
|
if i >= 2 {
|
|
// 模拟历史残留:挂载归属不存在的账号
|
|
m.AccountID = "no-such-account"
|
|
}
|
|
if err := repos.EmbyMount.Create(ctx, m); err != nil {
|
|
t.Fatalf("create mount %s: %v", vid, err)
|
|
}
|
|
}
|
|
|
|
svc.CleanupOrphanMounts(ctx)
|
|
|
|
left, err := repos.EmbyMount.List(ctx)
|
|
if err != nil {
|
|
t.Fatalf("list mounts: %v", err)
|
|
}
|
|
if len(left) != 2 {
|
|
t.Fatalf("mounts after cleanup = %d, want 2 (orphans removed)", len(left))
|
|
}
|
|
for _, m := range left {
|
|
if m.AccountID != acct.ID {
|
|
t.Fatalf("mount %s still orphan (account %s)", m.ID, m.AccountID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func mustJSON(v any) string {
|
|
data, err := json.Marshal(v)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return string(data)
|
|
}
|