- Refactor download.go and stream.go with downloadDeps/streamDeps structs for dependency injection, enabling unit testing without real I/O - download_test.go: 15 tests — input validation, mock downloaders, method selection, cobra Args, deadlock detection - stream_test.go: input validation, noOpen flag, engine error handling - client_test.go: context cancellation, timeout, full Sync roundtrip, watch-progress and HTTP error unwrapping - sync_test.go: TriggerSync on watching transition, adjustInterval - torrent_test.go: TorrentDownloader lifecycle without network - stream_server_test.go: HTTP server lifecycle, SetFile/ClearFile, concurrent requests, Shutdown releases port, content-type - manager_integration_test.go: full pipeline — success, torrent→debrid fallback, all-fail, multi-concurrent, ForceStart, OnTaskDone, recent-finished drain, cancel mid-download, organize - usenet_test.go: Cancel/Pause race regression test (run with -race) - daemon_test.go: isAllowedStreamPath table tests - CI: split coverage gate to engine+agent only (50% threshold); cmd coverage still reported but not gated (interactive UI commands) - lefthook: add pre-push hook with go test -race -count=1 -timeout=120s
165 lines
5.2 KiB
Go
165 lines
5.2 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"os/exec"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/torrentclaw/unarr/internal/engine"
|
|
)
|
|
|
|
// --- Tests de validación de entrada para runStream ---
|
|
|
|
func TestRunStream_EmptyInput(t *testing.T) {
|
|
err := runStream("", 0, true, "")
|
|
if err == nil {
|
|
t.Fatal("expected error for empty input")
|
|
}
|
|
}
|
|
|
|
func TestRunStream_InvalidInput_NotHashNotMagnet(t *testing.T) {
|
|
err := runStream("The Matrix 1999", 0, true, "")
|
|
if err == nil {
|
|
t.Fatal("expected error for plain text input")
|
|
}
|
|
if !strings.Contains(err.Error(), "invalid") {
|
|
t.Errorf("error = %q, want 'invalid' in message", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestRunStream_InvalidInput_TooShort(t *testing.T) {
|
|
err := runStream("abc123", 0, true, "")
|
|
if err == nil {
|
|
t.Fatal("expected error for hash too short")
|
|
}
|
|
}
|
|
|
|
func TestRunStream_ValidHash_PassesValidation(t *testing.T) {
|
|
// Un hash válido debe pasar la validación y llegar a newStreamEngine.
|
|
// Inyectamos un engine que falla inmediatamente para no necesitar red.
|
|
deps := streamDeps{
|
|
newStreamEngine: func(cfg engine.StreamConfig) (*engine.StreamEngine, error) {
|
|
return nil, fmt.Errorf("test: stopping after validation")
|
|
},
|
|
newStreamServer: engine.NewStreamServer,
|
|
openPlayer: func(url, override string) (string, *exec.Cmd, error) {
|
|
return "", nil, nil
|
|
},
|
|
}
|
|
|
|
err := runStreamWithDeps("abc123def456abc123def456abc123def456abc1", 0, true, "", deps)
|
|
if err == nil {
|
|
t.Fatal("expected error from newStreamEngine mock")
|
|
}
|
|
// El error debe venir del engine, no de validación
|
|
if strings.Contains(err.Error(), "invalid input") {
|
|
t.Errorf("error = %q — should not be a validation error, hash is valid", err.Error())
|
|
}
|
|
if !strings.Contains(err.Error(), "create stream engine") {
|
|
t.Errorf("error = %q — expected 'create stream engine' from engine creation failure", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestRunStream_MagnetURI_PassesValidation(t *testing.T) {
|
|
deps := streamDeps{
|
|
newStreamEngine: func(cfg engine.StreamConfig) (*engine.StreamEngine, error) {
|
|
return nil, fmt.Errorf("test: stopping after validation")
|
|
},
|
|
newStreamServer: engine.NewStreamServer,
|
|
openPlayer: func(url, override string) (string, *exec.Cmd, error) {
|
|
return "", nil, nil
|
|
},
|
|
}
|
|
|
|
magnet := "magnet:?xt=urn:btih:abc123def456abc123def456abc123def456abc1&dn=Test"
|
|
err := runStreamWithDeps(magnet, 0, true, "", deps)
|
|
if err == nil {
|
|
t.Fatal("expected error from newStreamEngine mock")
|
|
}
|
|
if strings.Contains(err.Error(), "invalid input") {
|
|
t.Errorf("magnet URI should be valid, got validation error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunStream_EngineCreationFails(t *testing.T) {
|
|
deps := streamDeps{
|
|
newStreamEngine: func(cfg engine.StreamConfig) (*engine.StreamEngine, error) {
|
|
return nil, fmt.Errorf("failed to create torrent client")
|
|
},
|
|
newStreamServer: engine.NewStreamServer,
|
|
openPlayer: func(url, override string) (string, *exec.Cmd, error) {
|
|
return "", nil, nil
|
|
},
|
|
}
|
|
|
|
err := runStreamWithDeps("abc123def456abc123def456abc123def456abc1", 0, true, "", deps)
|
|
if err == nil {
|
|
t.Fatal("expected error when engine creation fails")
|
|
}
|
|
if !strings.Contains(err.Error(), "create stream engine") {
|
|
t.Errorf("error = %q, want 'create stream engine' in message", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestRunStreamCmd_Args_TooFew(t *testing.T) {
|
|
cmd := newStreamCmd()
|
|
err := cmd.Args(cmd, []string{})
|
|
if err == nil {
|
|
t.Fatal("expected error for 0 args")
|
|
}
|
|
}
|
|
|
|
func TestRunStreamCmd_Args_TooMany(t *testing.T) {
|
|
cmd := newStreamCmd()
|
|
err := cmd.Args(cmd, []string{"hash1", "hash2"})
|
|
if err == nil {
|
|
t.Fatal("expected error for 2 args")
|
|
}
|
|
}
|
|
|
|
func TestRunStreamCmd_Args_ExactlyOne(t *testing.T) {
|
|
cmd := newStreamCmd()
|
|
err := cmd.Args(cmd, []string{"abc123def456abc123def456abc123def456abc1"})
|
|
if err != nil {
|
|
t.Errorf("unexpected error for 1 arg: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunStream_PartialMagnet_Prefix(t *testing.T) {
|
|
// "magnet:" sin hash es válido para el parser (tiene el prefijo magnet:)
|
|
// pero no tiene infoHash — debe pasar la validación de input
|
|
deps := streamDeps{
|
|
newStreamEngine: func(cfg engine.StreamConfig) (*engine.StreamEngine, error) {
|
|
return nil, fmt.Errorf("test stop")
|
|
},
|
|
newStreamServer: engine.NewStreamServer,
|
|
openPlayer: func(url, override string) (string, *exec.Cmd, error) { return "", nil, nil },
|
|
}
|
|
// "magnet:" sin btih se trata como magnet (HasPrefix("magnet:") == true)
|
|
// por lo que pasa la validación de input
|
|
err := runStreamWithDeps("magnet:", 0, true, "", deps)
|
|
// Debe llegar al engine (validación OK) o fallar con error de engine
|
|
_ = err // no verificamos el contenido exacto, solo que no haya panic
|
|
}
|
|
|
|
func TestRunStream_NoOpen_DoesNotCallOpenPlayer(t *testing.T) {
|
|
playerCalled := false
|
|
deps := streamDeps{
|
|
newStreamEngine: func(cfg engine.StreamConfig) (*engine.StreamEngine, error) {
|
|
return nil, fmt.Errorf("test: stopping early")
|
|
},
|
|
newStreamServer: engine.NewStreamServer,
|
|
openPlayer: func(url, override string) (string, *exec.Cmd, error) {
|
|
playerCalled = true
|
|
return "mpv", nil, nil
|
|
},
|
|
}
|
|
|
|
// noOpen=true → openPlayer no debe llamarse
|
|
runStreamWithDeps("abc123def456abc123def456abc123def456abc1", 0, true, "", deps) //nolint:errcheck
|
|
|
|
if playerCalled {
|
|
t.Error("openPlayer should NOT be called when noOpen=true")
|
|
}
|
|
}
|