unarr/internal/cmd/stream_test.go
Deivid Soto 78c16c295e test: add comprehensive test suite for engine, agent and cmd packages
- 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
2026-04-08 23:36:00 +02:00

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")
}
}