package agent import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" ) func TestRegister(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Errorf("method = %s, want POST", r.Method) } if r.URL.Path != "/api/internal/agent/register" { t.Errorf("path = %s, want /api/internal/agent/register", r.URL.Path) } if r.Header.Get("Authorization") != "Bearer test-key" { t.Errorf("auth = %q, want Bearer test-key", r.Header.Get("Authorization")) } if r.Header.Get("Content-Type") != "application/json" { t.Errorf("content-type = %q, want application/json", r.Header.Get("Content-Type")) } var req RegisterRequest json.NewDecoder(r.Body).Decode(&req) if req.AgentID != "agent-123" { t.Errorf("agentId = %q, want agent-123", req.AgentID) } json.NewEncoder(w).Encode(RegisterResponse{ Success: true, User: UserInfo{Name: "David", Email: "d@test.com", Plan: "pro", IsPro: true}, Features: FeatureFlags{ Debrid: true, Usenet: false, Torrent: true, }, }) })) defer srv.Close() c := NewClient(srv.URL, "test-key", "unarr-test") resp, err := c.Register(context.Background(), RegisterRequest{ AgentID: "agent-123", Name: "Test Machine", OS: "linux", Arch: "amd64", Version: "0.2.0", }) if err != nil { t.Fatalf("Register failed: %v", err) } if !resp.Success { t.Error("expected Success=true") } if resp.User.Name != "David" { t.Errorf("user.name = %q, want David", resp.User.Name) } if !resp.User.IsPro { t.Error("expected IsPro=true") } if !resp.Features.Debrid { t.Error("expected debrid=true") } if !resp.Features.Torrent { t.Error("expected torrent=true") } if resp.Features.Usenet { t.Error("expected usenet=false") } } func TestHeartbeat(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/internal/agent/heartbeat" { t.Errorf("path = %s, want /api/internal/agent/heartbeat", r.URL.Path) } var req HeartbeatRequest json.NewDecoder(r.Body).Decode(&req) if req.AgentID != "agent-123" { t.Errorf("agentId = %q, want agent-123", req.AgentID) } json.NewEncoder(w).Encode(HeartbeatResponse{Success: true}) })) defer srv.Close() c := NewClient(srv.URL, "test-key", "unarr-test") resp, err := c.Heartbeat(context.Background(), HeartbeatRequest{AgentID: "agent-123"}) if err != nil { t.Fatalf("Heartbeat failed: %v", err) } if !resp.Success { t.Error("expected success=true") } } func TestClaimTasks(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { t.Errorf("method = %s, want GET", r.Method) } if r.URL.Query().Get("agentId") != "agent-123" { t.Errorf("agentId param = %q, want agent-123", r.URL.Query().Get("agentId")) } json.NewEncoder(w).Encode(TasksResponse{ Tasks: []Task{ { ID: "task-uuid-1", InfoHash: "abc123def456abc123def456abc123def456abc1", Title: "The Matrix (1999)", PreferredMethod: "auto", }, }, }) })) defer srv.Close() c := NewClient(srv.URL, "test-key", "unarr-test") resp, err := c.ClaimTasks(context.Background(), "agent-123") if err != nil { t.Fatalf("ClaimTasks failed: %v", err) } if len(resp.Tasks) != 1 { t.Fatalf("len(tasks) = %d, want 1", len(resp.Tasks)) } if resp.Tasks[0].ID != "task-uuid-1" { t.Errorf("task.ID = %q, want task-uuid-1", resp.Tasks[0].ID) } if resp.Tasks[0].InfoHash != "abc123def456abc123def456abc123def456abc1" { t.Errorf("task.InfoHash = %q", resp.Tasks[0].InfoHash) } if resp.Tasks[0].PreferredMethod != "auto" { t.Errorf("task.PreferredMethod = %q, want auto", resp.Tasks[0].PreferredMethod) } } func TestReportStatus(t *testing.T) { var received StatusUpdate srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/internal/agent/status" { t.Errorf("path = %s, want /api/internal/agent/status", r.URL.Path) } json.NewDecoder(r.Body).Decode(&received) json.NewEncoder(w).Encode(StatusResponse{Success: true}) })) defer srv.Close() c := NewClient(srv.URL, "test-key", "unarr-test") _, err := c.ReportStatus(context.Background(), StatusUpdate{ TaskID: "task-uuid-1", Status: "downloading", Progress: 42, DownloadedBytes: 1073741824, TotalBytes: 2147483648, SpeedBps: 5242880, ETA: 120, ResolvedMethod: "torrent", FileName: "The.Matrix.1999.1080p.mkv", }) if err != nil { t.Fatalf("ReportStatus failed: %v", err) } if received.TaskID != "task-uuid-1" { t.Errorf("taskId = %q, want task-uuid-1", received.TaskID) } if received.Progress != 42 { t.Errorf("progress = %d, want 42", received.Progress) } if received.ResolvedMethod != "torrent" { t.Errorf("resolvedMethod = %q, want torrent", received.ResolvedMethod) } } func TestClaimTasksEmpty(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(TasksResponse{Tasks: []Task{}}) })) defer srv.Close() c := NewClient(srv.URL, "test-key", "unarr-test") resp, err := c.ClaimTasks(context.Background(), "agent-123") if err != nil { t.Fatalf("ClaimTasks failed: %v", err) } if len(resp.Tasks) != 0 { t.Errorf("expected empty tasks, got %d", len(resp.Tasks)) } } func TestAPIError(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(ErrorResponse{Error: "Invalid API key"}) })) defer srv.Close() c := NewClient(srv.URL, "bad-key", "unarr-test") _, err := c.Register(context.Background(), RegisterRequest{AgentID: "x"}) if err == nil { t.Fatal("expected error for 401 response") } if got := err.Error(); got == "" { t.Error("error message should not be empty") } } func TestAPIError404(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(ErrorResponse{Error: "Task not found"}) })) defer srv.Close() c := NewClient(srv.URL, "test-key", "unarr-test") _, err := c.ReportStatus(context.Background(), StatusUpdate{TaskID: "missing"}) if err == nil { t.Fatal("expected error for 404 response") } } func TestReportStatusCancelled(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(StatusResponse{Success: true, Cancelled: true}) })) defer srv.Close() c := NewClient(srv.URL, "test-key", "unarr-test") resp, err := c.ReportStatus(context.Background(), StatusUpdate{TaskID: "task-1", Status: "downloading"}) if err != nil { t.Fatalf("ReportStatus failed: %v", err) } if !resp.Cancelled { t.Error("expected cancelled=true") } } func TestReportStatusPaused(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(StatusResponse{Success: true, Paused: true}) })) defer srv.Close() c := NewClient(srv.URL, "test-key", "unarr-test") resp, err := c.ReportStatus(context.Background(), StatusUpdate{TaskID: "task-1", Status: "downloading"}) if err != nil { t.Fatalf("ReportStatus failed: %v", err) } if !resp.Paused { t.Error("expected paused=true") } if resp.Cancelled { t.Error("expected cancelled=false") } } func TestReportStatusDeleteFiles(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(StatusResponse{Success: true, Cancelled: true, DeleteFiles: true}) })) defer srv.Close() c := NewClient(srv.URL, "test-key", "unarr-test") resp, err := c.ReportStatus(context.Background(), StatusUpdate{TaskID: "task-1"}) if err != nil { t.Fatalf("ReportStatus failed: %v", err) } if !resp.Cancelled { t.Error("expected cancelled=true") } if !resp.DeleteFiles { t.Error("expected deleteFiles=true") } } func TestUserAgent(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("User-Agent") != "unarr/0.2.0" { t.Errorf("User-Agent = %q, want unarr/0.2.0", r.Header.Get("User-Agent")) } json.NewEncoder(w).Encode(HeartbeatResponse{Success: true}) })) defer srv.Close() c := NewClient(srv.URL, "test-key", "unarr/0.2.0") c.Heartbeat(context.Background(), HeartbeatRequest{AgentID: "x"}) } func TestHeartbeatWithUpgradeSignal(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(HeartbeatResponse{ Success: true, Upgrade: &UpgradeSignal{Version: "2.0.0"}, }) })) defer srv.Close() c := NewClient(srv.URL, "test-key", "unarr-test") resp, err := c.Heartbeat(context.Background(), HeartbeatRequest{AgentID: "agent-1"}) if err != nil { t.Fatalf("Heartbeat failed: %v", err) } if resp.Upgrade == nil { t.Fatal("expected upgrade signal, got nil") } if resp.Upgrade.Version != "2.0.0" { t.Errorf("upgrade version = %q, want 2.0.0", resp.Upgrade.Version) } } func TestHeartbeatWithoutUpgradeSignal(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(HeartbeatResponse{Success: true}) })) defer srv.Close() c := NewClient(srv.URL, "test-key", "unarr-test") resp, err := c.Heartbeat(context.Background(), HeartbeatRequest{AgentID: "agent-1"}) if err != nil { t.Fatalf("Heartbeat failed: %v", err) } if resp.Upgrade != nil { t.Errorf("expected no upgrade signal, got %+v", resp.Upgrade) } } func TestDeregister(t *testing.T) { var received struct { AgentID string `json:"agentId"` } srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/internal/agent/deregister" { t.Errorf("path = %s", r.URL.Path) } if r.Method != http.MethodPost { t.Errorf("method = %s, want POST", r.Method) } json.NewDecoder(r.Body).Decode(&received) json.NewEncoder(w).Encode(StatusResponse{Success: true}) })) defer srv.Close() c := NewClient(srv.URL, "test-key", "unarr-test") err := c.Deregister(context.Background(), "agent-42") if err != nil { t.Fatalf("Deregister failed: %v", err) } if received.AgentID != "agent-42" { t.Errorf("agentId = %q, want agent-42", received.AgentID) } } func TestBatchReportStatus(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/internal/agent/status" { t.Errorf("path = %s", r.URL.Path) } var req BatchStatusRequest json.NewDecoder(r.Body).Decode(&req) if len(req.Updates) != 2 { t.Errorf("expected 2 updates, got %d", len(req.Updates)) } json.NewEncoder(w).Encode(BatchStatusResponse{ Results: []StatusResponse{ {Success: true}, {Success: true, Cancelled: true}, }, }) })) defer srv.Close() c := NewClient(srv.URL, "test-key", "unarr-test") resp, err := c.BatchReportStatus(context.Background(), []StatusUpdate{ {TaskID: "t1", Status: "downloading"}, {TaskID: "t2", Status: "completed"}, }) if err != nil { t.Fatalf("BatchReportStatus failed: %v", err) } if len(resp.Results) != 2 { t.Fatalf("expected 2 results, got %d", len(resp.Results)) } if !resp.Results[1].Cancelled { t.Error("expected result[1].Cancelled=true") } } func TestSearchNzbs(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/internal/agent/nzb-search" { t.Errorf("path = %s", r.URL.Path) } json.NewEncoder(w).Encode(NzbSearchResponse{ Results: []NzbSearchResult{ {NzbID: "nzb-1", Title: "Movie.2023.1080p"}, }, }) })) defer srv.Close() c := NewClient(srv.URL, "test-key", "unarr-test") resp, err := c.SearchNzbs(context.Background(), NzbSearchParams{Query: "Movie"}) if err != nil { t.Fatalf("SearchNzbs failed: %v", err) } if len(resp.Results) != 1 { t.Fatalf("expected 1 result, got %d", len(resp.Results)) } if resp.Results[0].NzbID != "nzb-1" { t.Errorf("nzb ID = %q, want nzb-1", resp.Results[0].NzbID) } } func TestDownloadNzb(t *testing.T) { nzbContent := []byte(`test`) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/internal/agent/nzb-download" { t.Errorf("path = %s", r.URL.Path) } if r.URL.Query().Get("nzbId") != "nzb-42" { t.Errorf("nzbId = %q, want nzb-42", r.URL.Query().Get("nzbId")) } w.Write(nzbContent) })) defer srv.Close() c := NewClient(srv.URL, "test-key", "unarr-test") data, err := c.DownloadNzb(context.Background(), "nzb-42") if err != nil { t.Fatalf("DownloadNzb failed: %v", err) } if string(data) != string(nzbContent) { t.Errorf("nzb content mismatch") } } func TestDownloadNzbError(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) w.Write([]byte("NZB not found")) })) defer srv.Close() c := NewClient(srv.URL, "test-key", "unarr-test") _, err := c.DownloadNzb(context.Background(), "bad-id") if err == nil { t.Fatal("expected error for 404 response") } } func TestGetUsenetCredentials(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/internal/agent/usenet-credentials" { t.Errorf("path = %s", r.URL.Path) } json.NewEncoder(w).Encode(UsenetCredentials{ Host: "news.example.com", Port: 563, SSL: true, Username: "user1", Password: "pass1", MaxConnections: 10, }) })) defer srv.Close() c := NewClient(srv.URL, "test-key", "unarr-test") creds, err := c.GetUsenetCredentials(context.Background()) if err != nil { t.Fatalf("GetUsenetCredentials failed: %v", err) } if creds.Host != "news.example.com" { t.Errorf("host = %q, want news.example.com", creds.Host) } if creds.Username != "user1" { t.Errorf("username = %q, want user1", creds.Username) } if creds.MaxConnections != 10 { t.Errorf("maxConnections = %d, want 10", creds.MaxConnections) } } func TestGetUsenetUsage(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/internal/agent/usenet-usage" { t.Errorf("path = %s", r.URL.Path) } json.NewEncoder(w).Encode(UsenetUsageResponse{ UsedBytes: 5368709120, QuotaBytes: 10737418240, }) })) defer srv.Close() c := NewClient(srv.URL, "test-key", "unarr-test") usage, err := c.GetUsenetUsage(context.Background()) if err != nil { t.Fatalf("GetUsenetUsage failed: %v", err) } if usage.UsedBytes != 5368709120 { t.Errorf("usedBytes = %d", usage.UsedBytes) } } func TestConfigureDebrid(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/internal/agent/debrid-config" { t.Errorf("path = %s", r.URL.Path) } json.NewEncoder(w).Encode(ConfigureDebridResponse{Success: true}) })) defer srv.Close() c := NewClient(srv.URL, "test-key", "unarr-test") resp, err := c.ConfigureDebrid(context.Background(), ConfigureDebridRequest{ Provider: "real-debrid", Token: "rd-token-123", }) if err != nil { t.Fatalf("ConfigureDebrid failed: %v", err) } if !resp.Success { t.Error("expected success=true") } } func TestBatchDownload(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/internal/agent/batch-download" { t.Errorf("path = %s", r.URL.Path) } json.NewEncoder(w).Encode(BatchDownloadResponse{ Queued: 3, NotFound: 1, }) })) defer srv.Close() c := NewClient(srv.URL, "test-key", "unarr-test") resp, err := c.BatchDownload(context.Background(), BatchDownloadRequest{}) if err != nil { t.Fatalf("BatchDownload failed: %v", err) } if resp.Queued != 3 { t.Errorf("queued = %d, want 3", resp.Queued) } } func TestSyncLibrary(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/internal/agent/library-sync" { t.Errorf("path = %s", r.URL.Path) } json.NewEncoder(w).Encode(LibrarySyncResponse{ Matched: 10, Synced: 15, Removed: 2, }) })) defer srv.Close() c := NewClient(srv.URL, "test-key", "unarr-test") resp, err := c.SyncLibrary(context.Background(), LibrarySyncRequest{}) if err != nil { t.Fatalf("SyncLibrary failed: %v", err) } if resp.Matched != 10 { t.Errorf("matched = %d, want 10", resp.Matched) } if resp.Synced != 15 { t.Errorf("synced = %d, want 15", resp.Synced) } } func TestHTMLErrorResponse(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadGateway) w.Write([]byte("502 Bad Gateway")) })) defer srv.Close() c := NewClient(srv.URL, "test-key", "unarr-test") _, err := c.Register(context.Background(), RegisterRequest{AgentID: "x"}) if err == nil { t.Fatal("expected error for HTML error page") } }