270 lines
6.7 KiB
Go
270 lines
6.7 KiB
Go
package agent
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
func TestLocalState_UpdateAndSnapshot(t *testing.T) {
|
|
s := NewLocalState()
|
|
|
|
s.Update(TaskState{TaskID: "t1", Status: "downloading", Progress: 50})
|
|
s.Update(TaskState{TaskID: "t2", Status: "completed", Progress: 100})
|
|
|
|
snap := s.Snapshot()
|
|
if len(snap) != 2 {
|
|
t.Fatalf("expected 2 tasks, got %d", len(snap))
|
|
}
|
|
|
|
byID := make(map[string]TaskState, len(snap))
|
|
for _, ts := range snap {
|
|
byID[ts.TaskID] = ts
|
|
}
|
|
|
|
if byID["t1"].Progress != 50 {
|
|
t.Errorf("expected progress 50, got %d", byID["t1"].Progress)
|
|
}
|
|
if byID["t2"].Status != "completed" {
|
|
t.Errorf("expected completed, got %s", byID["t2"].Status)
|
|
}
|
|
}
|
|
|
|
func TestLocalState_UpdateOverwrites(t *testing.T) {
|
|
s := NewLocalState()
|
|
|
|
s.Update(TaskState{TaskID: "t1", Status: "downloading", Progress: 30})
|
|
s.Update(TaskState{TaskID: "t1", Status: "downloading", Progress: 70})
|
|
|
|
snap := s.Snapshot()
|
|
if len(snap) != 1 {
|
|
t.Fatalf("expected 1 task, got %d", len(snap))
|
|
}
|
|
if snap[0].Progress != 70 {
|
|
t.Errorf("expected progress 70, got %d", snap[0].Progress)
|
|
}
|
|
}
|
|
|
|
func TestLocalState_Remove(t *testing.T) {
|
|
s := NewLocalState()
|
|
|
|
s.Update(TaskState{TaskID: "t1", Status: "downloading"})
|
|
s.Update(TaskState{TaskID: "t2", Status: "downloading"})
|
|
s.Remove("t1")
|
|
|
|
snap := s.Snapshot()
|
|
if len(snap) != 1 {
|
|
t.Fatalf("expected 1 task, got %d", len(snap))
|
|
}
|
|
if snap[0].TaskID != "t2" {
|
|
t.Errorf("expected t2, got %s", snap[0].TaskID)
|
|
}
|
|
}
|
|
|
|
func TestLocalState_RemoveNonExistent(t *testing.T) {
|
|
s := NewLocalState()
|
|
s.Remove("nonexistent") // should not panic
|
|
}
|
|
|
|
func TestLocalState_SnapshotIsACopy(t *testing.T) {
|
|
s := NewLocalState()
|
|
s.Update(TaskState{TaskID: "t1", Status: "downloading", Progress: 50})
|
|
|
|
snap := s.Snapshot()
|
|
snap[0].Progress = 999
|
|
|
|
snap2 := s.Snapshot()
|
|
if snap2[0].Progress != 50 {
|
|
t.Errorf("snapshot mutation leaked: got progress %d", snap2[0].Progress)
|
|
}
|
|
}
|
|
|
|
func TestLocalState_UpdateSetsTimestamp(t *testing.T) {
|
|
s := NewLocalState()
|
|
s.Update(TaskState{TaskID: "t1", Status: "downloading"})
|
|
|
|
snap := s.Snapshot()
|
|
if snap[0].UpdatedAt == 0 {
|
|
t.Error("expected non-zero UpdatedAt")
|
|
}
|
|
}
|
|
|
|
func TestLocalState_ConcurrentAccess(t *testing.T) {
|
|
s := NewLocalState()
|
|
var wg sync.WaitGroup
|
|
|
|
for i := range 100 {
|
|
wg.Add(1)
|
|
go func(n int) {
|
|
defer wg.Done()
|
|
taskID := "t" + string(rune('0'+n%10))
|
|
s.Update(TaskState{TaskID: taskID, Status: "downloading", Progress: n})
|
|
s.Snapshot()
|
|
if n%3 == 0 {
|
|
s.Remove(taskID)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
// No race condition = test passes
|
|
}
|
|
|
|
func TestLocalState_WriteToDisk_ReadFromDisk(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "tasks.json")
|
|
|
|
// Override the file path for testing
|
|
orig := taskStateFilePathFn
|
|
taskStateFilePathFn = func() string { return path }
|
|
defer func() { taskStateFilePathFn = orig }()
|
|
|
|
s := NewLocalState()
|
|
s.Update(TaskState{TaskID: "t1", Status: "downloading", Progress: 45})
|
|
s.Update(TaskState{TaskID: "t2", Status: "completed", Progress: 100, FilePath: "/tmp/movie.mkv"})
|
|
s.WriteToDisk()
|
|
|
|
// Verify file exists
|
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
|
t.Fatal("tasks.json was not created")
|
|
}
|
|
|
|
// Read into a new LocalState
|
|
s2 := NewLocalState()
|
|
s2.ReadFromDisk()
|
|
|
|
snap := s2.Snapshot()
|
|
if len(snap) != 2 {
|
|
t.Fatalf("expected 2 tasks after read, got %d", len(snap))
|
|
}
|
|
|
|
byID := make(map[string]TaskState, len(snap))
|
|
for _, ts := range snap {
|
|
byID[ts.TaskID] = ts
|
|
}
|
|
|
|
if byID["t1"].Progress != 45 {
|
|
t.Errorf("expected progress 45, got %d", byID["t1"].Progress)
|
|
}
|
|
if byID["t2"].FilePath != "/tmp/movie.mkv" {
|
|
t.Errorf("expected /tmp/movie.mkv, got %s", byID["t2"].FilePath)
|
|
}
|
|
}
|
|
|
|
func TestLocalState_ReadFromDisk_CorruptedFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "tasks.json")
|
|
|
|
orig := taskStateFilePathFn
|
|
taskStateFilePathFn = func() string { return path }
|
|
defer func() { taskStateFilePathFn = orig }()
|
|
|
|
// Write corrupted JSON
|
|
os.WriteFile(path, []byte("{invalid json"), 0o644)
|
|
|
|
s := NewLocalState()
|
|
s.ReadFromDisk() // should not panic
|
|
|
|
snap := s.Snapshot()
|
|
if len(snap) != 0 {
|
|
t.Errorf("expected 0 tasks from corrupted file, got %d", len(snap))
|
|
}
|
|
}
|
|
|
|
func TestLocalState_ReadFromDisk_FileNotFound(t *testing.T) {
|
|
orig := taskStateFilePathFn
|
|
taskStateFilePathFn = func() string { return "/nonexistent/path/tasks.json" }
|
|
defer func() { taskStateFilePathFn = orig }()
|
|
|
|
s := NewLocalState()
|
|
s.ReadFromDisk() // should not panic
|
|
|
|
snap := s.Snapshot()
|
|
if len(snap) != 0 {
|
|
t.Errorf("expected 0 tasks, got %d", len(snap))
|
|
}
|
|
}
|
|
|
|
func TestLocalState_AtomicWrite(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "tasks.json")
|
|
|
|
orig := taskStateFilePathFn
|
|
taskStateFilePathFn = func() string { return path }
|
|
defer func() { taskStateFilePathFn = orig }()
|
|
|
|
s := NewLocalState()
|
|
s.Update(TaskState{TaskID: "t1", Status: "downloading"})
|
|
s.WriteToDisk()
|
|
|
|
// Verify no .tmp file remains
|
|
tmpPath := path + ".tmp"
|
|
if _, err := os.Stat(tmpPath); !os.IsNotExist(err) {
|
|
t.Error("temp file should not exist after write")
|
|
}
|
|
}
|
|
|
|
func TestLocalState_EmptySnapshot(t *testing.T) {
|
|
s := NewLocalState()
|
|
snap := s.Snapshot()
|
|
if snap == nil {
|
|
t.Error("snapshot should be non-nil empty slice")
|
|
}
|
|
if len(snap) != 0 {
|
|
t.Errorf("expected 0 tasks, got %d", len(snap))
|
|
}
|
|
}
|
|
|
|
func TestTaskStateFromUpdate(t *testing.T) {
|
|
u := StatusUpdate{
|
|
TaskID: "task-1",
|
|
Status: "downloading",
|
|
Progress: 42,
|
|
DownloadedBytes: 1024,
|
|
TotalBytes: 4096,
|
|
SpeedBps: 100,
|
|
ETA: 30,
|
|
ResolvedMethod: "torrent",
|
|
FileName: "movie.mkv",
|
|
FilePath: "/tmp/movie.mkv",
|
|
StreamURL: "http://localhost/stream",
|
|
ErrorMessage: "",
|
|
}
|
|
got := TaskStateFromUpdate(u)
|
|
if got.TaskID != "task-1" || got.Status != "downloading" || got.Progress != 42 {
|
|
t.Errorf("basic fields wrong: %+v", got)
|
|
}
|
|
if got.DownloadedBytes != 1024 || got.TotalBytes != 4096 || got.SpeedBps != 100 {
|
|
t.Errorf("byte fields wrong: %+v", got)
|
|
}
|
|
if got.ResolvedMethod != "torrent" || got.FileName != "movie.mkv" {
|
|
t.Errorf("method/name fields wrong: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestShortID(t *testing.T) {
|
|
if got := ShortID("abcdef1234567890"); got != "abcdef12" {
|
|
t.Errorf("ShortID = %q", got)
|
|
}
|
|
if got := ShortID("short"); got != "short" {
|
|
t.Errorf("ShortID short = %q", got)
|
|
}
|
|
if got := ShortID(""); got != "" {
|
|
t.Errorf("ShortID empty = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestStateFilePath(t *testing.T) {
|
|
if got := StateFilePath(); got == "" {
|
|
t.Errorf("StateFilePath should not be empty")
|
|
}
|
|
}
|
|
|
|
func TestHTTPError(t *testing.T) {
|
|
e := &HTTPError{StatusCode: 404, Message: "not found"}
|
|
got := e.Error()
|
|
if got == "" || got == "API error 0: " {
|
|
t.Errorf("HTTPError.Error() unexpected: %q", got)
|
|
}
|
|
}
|