mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
42ef19b7e5
Fixes #65
418 lines
15 KiB
Go
418 lines
15 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sort"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/ShukeBta/MediaStationGo/internal/model"
|
|
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
|
)
|
|
|
|
func TestAddDownloadUsesDefaultTransmissionClient(t *testing.T) {
|
|
var mu sync.Mutex
|
|
var added map[string]interface{}
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodGet {
|
|
w.Header().Set("X-Transmission-Session-Id", "session-test")
|
|
w.WriteHeader(http.StatusConflict)
|
|
return
|
|
}
|
|
var req transmissionRPCRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
t.Errorf("decode transmission request: %v", err)
|
|
return
|
|
}
|
|
switch req.Method {
|
|
case "torrent-get":
|
|
_ = json.NewEncoder(w).Encode(transmissionRPCResponse{Result: "success", Arguments: map[string]interface{}{"torrents": []interface{}{}}})
|
|
case "torrent-add":
|
|
mu.Lock()
|
|
added = req.Arguments
|
|
mu.Unlock()
|
|
_ = json.NewEncoder(w).Encode(transmissionRPCResponse{
|
|
Result: "success",
|
|
Arguments: map[string]interface{}{
|
|
"torrent-added": map[string]interface{}{"hashString": "transmission-hash", "name": "Movie 2026"},
|
|
},
|
|
})
|
|
default:
|
|
t.Errorf("unexpected transmission method %q", req.Method)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
db := newServiceTestDB(t, &model.DownloadClient{}, &model.DownloadTask{}, &model.Setting{})
|
|
repos := repository.New(db)
|
|
client := &model.DownloadClient{Name: "Transmission", Type: "transmission", Host: server.URL, IsDefault: true, Enabled: true}
|
|
if err := repos.DownloadClient.Create(t.Context(), client); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
manager := NewDownloadManager(zap.NewNop(), repos, nil)
|
|
svc := NewDownloadService(zap.NewNop(), repos, NewHub(zap.NewNop()), nil)
|
|
svc.SetDownloadManager(manager)
|
|
|
|
task, err := svc.AddDownloadWithMeta(t.Context(), "user-1", "magnet:?xt=urn:btih:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&dn=Movie+2026", "/downloads/movies", DownloadTaskMeta{Title: "Movie 2026"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if task.Source != "transmission" || task.DownloadClientID != client.ID || task.ExternalID != "transmission-hash" {
|
|
t.Fatalf("task downloader identity = %#v", task)
|
|
}
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if added["filename"] != "magnet:?xt=urn:btih:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&dn=Movie+2026" {
|
|
t.Fatalf("transmission filename = %#v", added["filename"])
|
|
}
|
|
if added["download-dir"] != "/downloads/movies" {
|
|
t.Fatalf("transmission download-dir = %#v", added["download-dir"])
|
|
}
|
|
}
|
|
|
|
func TestAddDownloadSendsFetchedTorrentBytesToTransmission(t *testing.T) {
|
|
torrentData := []byte("d4:infod4:name7:fixtureee")
|
|
torrentServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("Content-Type", "application/x-bittorrent")
|
|
w.Header().Set("Content-Disposition", `attachment; filename="fixture.torrent"`)
|
|
_, _ = w.Write(torrentData)
|
|
}))
|
|
defer torrentServer.Close()
|
|
|
|
var metainfo string
|
|
transmission := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodGet {
|
|
w.Header().Set("X-Transmission-Session-Id", "session-test")
|
|
w.WriteHeader(http.StatusConflict)
|
|
return
|
|
}
|
|
var req transmissionRPCRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
t.Errorf("decode transmission request: %v", err)
|
|
return
|
|
}
|
|
arguments := map[string]interface{}{}
|
|
switch req.Method {
|
|
case "torrent-get":
|
|
arguments["torrents"] = []interface{}{}
|
|
case "torrent-add":
|
|
metainfo, _ = req.Arguments["metainfo"].(string)
|
|
arguments["torrent-added"] = map[string]interface{}{"hashString": "torrent-file-hash"}
|
|
}
|
|
_ = json.NewEncoder(w).Encode(transmissionRPCResponse{Result: "success", Arguments: arguments})
|
|
}))
|
|
defer transmission.Close()
|
|
|
|
db := newServiceTestDB(t, &model.Site{}, &model.DownloadClient{}, &model.DownloadTask{}, &model.Setting{})
|
|
repos := repository.New(db)
|
|
if err := repos.Site.Create(t.Context(), &model.Site{Name: "Fixture", Type: "custom_rss", URL: torrentServer.URL, AuthType: "cookie", Enabled: true}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
client := &model.DownloadClient{Name: "Transmission", Type: "transmission", Host: transmission.URL, IsDefault: true, Enabled: true}
|
|
if err := repos.DownloadClient.Create(t.Context(), client); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
manager := NewDownloadManager(zap.NewNop(), repos, nil)
|
|
site := NewSiteService(zap.NewNop(), repos, "")
|
|
svc := NewDownloadService(zap.NewNop(), repos, NewHub(zap.NewNop()), nil, site)
|
|
svc.SetDownloadManager(manager)
|
|
task, err := svc.AddDownloadWithMeta(t.Context(), "user-1", torrentServer.URL+"/fixture.torrent", "/downloads", DownloadTaskMeta{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if metainfo != base64.StdEncoding.EncodeToString(torrentData) {
|
|
t.Fatalf("metainfo = %q", metainfo)
|
|
}
|
|
if task.ExternalID != "torrent-file-hash" || task.Title != "fixture" {
|
|
t.Fatalf("task = %#v", task)
|
|
}
|
|
}
|
|
|
|
func TestAddDownloadSendsPublicTorrentURLBytesToAria2(t *testing.T) {
|
|
torrentData := []byte("d4:infod4:name7:fixtureee")
|
|
torrentServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("Content-Type", "application/x-bittorrent")
|
|
_, _ = w.Write(torrentData)
|
|
}))
|
|
defer torrentServer.Close()
|
|
|
|
var addMethod string
|
|
aria2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var req aria2Request
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
t.Errorf("decode aria2 request: %v", err)
|
|
return
|
|
}
|
|
result := interface{}(map[string]interface{}{"version": "1.37"})
|
|
switch req.Method {
|
|
case "aria2.tellActive", "aria2.tellWaiting", "aria2.tellStopped":
|
|
result = []interface{}{}
|
|
case "aria2.addTorrent", "aria2.addUri":
|
|
addMethod = req.Method
|
|
result = "aria2-torrent-gid"
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{"jsonrpc": "2.0", "id": req.ID, "result": result})
|
|
}))
|
|
defer aria2.Close()
|
|
|
|
db := newServiceTestDB(t, &model.Site{}, &model.DownloadClient{}, &model.DownloadTask{}, &model.Setting{})
|
|
repos := repository.New(db)
|
|
client := &model.DownloadClient{Name: "aria2", Type: "aria2", Host: aria2.URL, IsDefault: true, Enabled: true}
|
|
if err := repos.DownloadClient.Create(t.Context(), client); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
site := NewSiteService(zap.NewNop(), repos, "")
|
|
svc := NewDownloadService(zap.NewNop(), repos, NewHub(zap.NewNop()), nil, site)
|
|
svc.SetDownloadManager(NewDownloadManager(zap.NewNop(), repos, nil))
|
|
task, err := svc.AddDownloadWithMeta(t.Context(), "user-1", torrentServer.URL+"/public.torrent", "/downloads", DownloadTaskMeta{Title: "Public Torrent"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if addMethod != "aria2.addTorrent" || task.ExternalID != "aria2-torrent-gid" {
|
|
t.Fatalf("add method = %q task = %#v", addMethod, task)
|
|
}
|
|
}
|
|
|
|
func TestReloadConfigHotSwapsUpdatedTransmissionClient(t *testing.T) {
|
|
newServer := func(addCalls *int32, hash string) *httptest.Server {
|
|
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodGet {
|
|
w.Header().Set("X-Transmission-Session-Id", "session-test")
|
|
w.WriteHeader(http.StatusConflict)
|
|
return
|
|
}
|
|
var req transmissionRPCRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
t.Errorf("decode transmission request: %v", err)
|
|
return
|
|
}
|
|
arguments := map[string]interface{}{}
|
|
switch req.Method {
|
|
case "torrent-get":
|
|
arguments["torrents"] = []interface{}{}
|
|
case "torrent-add":
|
|
atomic.AddInt32(addCalls, 1)
|
|
arguments["torrent-added"] = map[string]interface{}{"hashString": hash}
|
|
}
|
|
_ = json.NewEncoder(w).Encode(transmissionRPCResponse{Result: "success", Arguments: arguments})
|
|
}))
|
|
}
|
|
var firstCalls, secondCalls int32
|
|
first := newServer(&firstCalls, "first-hash")
|
|
defer first.Close()
|
|
second := newServer(&secondCalls, "second-hash")
|
|
defer second.Close()
|
|
|
|
db := newServiceTestDB(t, &model.DownloadClient{}, &model.DownloadTask{}, &model.Setting{})
|
|
repos := repository.New(db)
|
|
client := &model.DownloadClient{Name: "Transmission", Type: "transmission", Host: first.URL, IsDefault: true, Enabled: true}
|
|
if err := repos.DownloadClient.Create(t.Context(), client); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
manager := NewDownloadManager(zap.NewNop(), repos, nil)
|
|
svc := NewDownloadService(zap.NewNop(), repos, NewHub(zap.NewNop()), nil)
|
|
svc.SetDownloadManager(manager)
|
|
if _, err := svc.AddDownloadWithMeta(t.Context(), "user-1", "magnet:?xt=urn:btih:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&dn=First+Movie", "/downloads", DownloadTaskMeta{Title: "First Movie"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
client.Host = second.URL
|
|
if err := repos.DownloadClient.Update(t.Context(), client); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
task, err := svc.AddDownloadWithMeta(t.Context(), "user-1", "magnet:?xt=urn:btih:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb&dn=Second+Movie", "/downloads", DownloadTaskMeta{Title: "Second Movie"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if atomic.LoadInt32(&firstCalls) != 1 || atomic.LoadInt32(&secondCalls) != 1 || task.ExternalID != "second-hash" {
|
|
t.Fatalf("hot reload calls = %d/%d task = %#v", firstCalls, secondCalls, task)
|
|
}
|
|
}
|
|
|
|
func TestDownloadManagerPersistsOldestEnabledClientAsDefault(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodGet {
|
|
w.Header().Set("X-Transmission-Session-Id", "session-test")
|
|
w.WriteHeader(http.StatusConflict)
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
defer server.Close()
|
|
|
|
db := newServiceTestDB(t, &model.DownloadClient{})
|
|
repos := repository.New(db)
|
|
first := &model.DownloadClient{
|
|
Base: model.Base{CreatedAt: time.Now().Add(-time.Hour)},
|
|
Name: "First Transmission",
|
|
Type: "transmission",
|
|
Host: server.URL,
|
|
Enabled: true,
|
|
IsDefault: false,
|
|
}
|
|
second := &model.DownloadClient{
|
|
Base: model.Base{CreatedAt: time.Now()},
|
|
Name: "Second Transmission",
|
|
Type: "transmission",
|
|
Host: server.URL,
|
|
Enabled: true,
|
|
IsDefault: false,
|
|
}
|
|
if err := repos.DownloadClient.Create(t.Context(), first); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repos.DownloadClient.Create(t.Context(), second); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
manager := NewDownloadManager(zap.NewNop(), repos, nil)
|
|
if err := manager.LoadAll(t.Context()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
selected, _, err := manager.GetDefault(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if selected.ID != first.ID {
|
|
t.Fatalf("default client = %#v", selected)
|
|
}
|
|
refreshed, err := repos.DownloadClient.FindByID(t.Context(), first.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if refreshed == nil || !refreshed.IsDefault {
|
|
t.Fatalf("persisted default = %#v", refreshed)
|
|
}
|
|
}
|
|
|
|
func TestRelocateRejectsNonQBitClient(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodGet {
|
|
w.Header().Set("X-Transmission-Session-Id", "session-test")
|
|
w.WriteHeader(http.StatusConflict)
|
|
return
|
|
}
|
|
t.Errorf("unexpected Transmission request during unsupported relocation")
|
|
}))
|
|
defer server.Close()
|
|
|
|
db := newServiceTestDB(t, &model.DownloadClient{}, &model.DownloadTask{}, &model.Setting{})
|
|
repos := repository.New(db)
|
|
client := &model.DownloadClient{Name: "Transmission", Type: "transmission", Host: server.URL, IsDefault: true, Enabled: true}
|
|
if err := repos.DownloadClient.Create(t.Context(), client); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
manager := NewDownloadManager(zap.NewNop(), repos, nil)
|
|
svc := NewDownloadService(zap.NewNop(), repos, NewHub(zap.NewNop()), nil)
|
|
svc.SetDownloadManager(manager)
|
|
if err := svc.ReloadConfig(t.Context()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err := svc.RelocateTorrent(t.Context(), "transmission-hash", "/new/location", client.ID)
|
|
if !errors.Is(err, ErrDownloadOperationUnsupported) {
|
|
t.Fatalf("err = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestListAggregatesEnabledClientsWithNormalizedProgress(t *testing.T) {
|
|
transmission := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodGet {
|
|
w.Header().Set("X-Transmission-Session-Id", "session-test")
|
|
w.WriteHeader(http.StatusConflict)
|
|
return
|
|
}
|
|
var req transmissionRPCRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
t.Errorf("decode transmission request: %v", err)
|
|
return
|
|
}
|
|
arguments := map[string]interface{}{}
|
|
if req.Method == "torrent-get" {
|
|
arguments["torrents"] = []map[string]interface{}{{
|
|
"hashString": "transmission-hash",
|
|
"name": "Transmission Movie",
|
|
"totalSize": 1000,
|
|
"percentDone": 0.5,
|
|
"rateDownload": 100,
|
|
"rateUpload": 10,
|
|
"status": 4,
|
|
"downloadDir": "/downloads/transmission",
|
|
"addedDate": 100,
|
|
"doneDate": 0,
|
|
}}
|
|
}
|
|
_ = json.NewEncoder(w).Encode(transmissionRPCResponse{Result: "success", Arguments: arguments})
|
|
}))
|
|
defer transmission.Close()
|
|
|
|
aria2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var req aria2Request
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
t.Errorf("decode aria2 request: %v", err)
|
|
return
|
|
}
|
|
result := interface{}(map[string]interface{}{"version": "1.37"})
|
|
switch req.Method {
|
|
case "aria2.tellActive":
|
|
result = []map[string]interface{}{{
|
|
"gid": "aria2-gid",
|
|
"bittorrent": map[string]interface{}{"info": map[string]interface{}{"name": "Aria Movie"}, "infoHash": "aria-info-hash"},
|
|
"totalLength": "2000",
|
|
"completedLength": "500",
|
|
"downloadSpeed": "200",
|
|
"uploadSpeed": "20",
|
|
"status": "active",
|
|
"dir": "/downloads/aria2",
|
|
}}
|
|
case "aria2.tellWaiting", "aria2.tellStopped":
|
|
result = []interface{}{}
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"jsonrpc": "2.0",
|
|
"id": req.ID,
|
|
"result": result,
|
|
})
|
|
}))
|
|
defer aria2.Close()
|
|
|
|
db := newServiceTestDB(t, &model.DownloadClient{}, &model.DownloadTask{}, &model.Setting{})
|
|
repos := repository.New(db)
|
|
transmissionClient := &model.DownloadClient{Name: "Transmission", Type: "transmission", Host: transmission.URL, IsDefault: true, Enabled: true}
|
|
aria2Client := &model.DownloadClient{Name: "aria2", Type: "aria2", Host: aria2.URL, Enabled: true}
|
|
if err := repos.DownloadClient.Create(t.Context(), transmissionClient); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repos.DownloadClient.Create(t.Context(), aria2Client); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
manager := NewDownloadManager(zap.NewNop(), repos, nil)
|
|
svc := NewDownloadService(zap.NewNop(), repos, NewHub(zap.NewNop()), nil)
|
|
svc.SetDownloadManager(manager)
|
|
if err := svc.ReloadConfig(t.Context()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, live, err := svc.List(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(live) != 2 {
|
|
t.Fatalf("live torrents = %#v", live)
|
|
}
|
|
sort.Slice(live, func(i, j int) bool { return live[i].Source < live[j].Source })
|
|
if live[0].Source != "aria2" || live[0].ClientID != aria2Client.ID || live[0].Progress != 0.25 || live[0].ContentPath != "/downloads/aria2/Aria Movie" {
|
|
t.Fatalf("aria2 live torrent = %#v", live[0])
|
|
}
|
|
if live[1].Source != "transmission" || live[1].ClientID != transmissionClient.ID || live[1].Progress != 0.5 || live[1].ContentPath != "/downloads/transmission/Transmission Movie" {
|
|
t.Fatalf("transmission live torrent = %#v", live[1])
|
|
}
|
|
}
|