package pool import ( "encoding/json" "strings" "testing" "time" "crypto-miner-server/internal/models" ) // ─── parseAndSetJob ─────────────────────────────────────────────────────────── func newTestProxy() *Proxy { return NewProxy(&Config{ Host: "pool.test", Port: 3333, Wallet: "89QUKeqsKEGfP9Vpiph8jEXc3YyVFN5dKeYdMFVraVG4SGU3jAprbBp9AgRutKxzPSdQQMp9EGeG7Wmh8NRfniiaMMYpmC3", Password: "x", }) } func TestParseAndSetJobDirectObject(t *testing.T) { p := newTestProxy() var received *Job p.SetCallbacks(func(j *Job) { received = j }, nil, nil) data := json.RawMessage(`{ "job_id":"abc123","height":3688000,"blob":"0c0caffee", "target":"ffffffff","seed_hash":"aabbccdd","difficulty":100000,"algo":"rx/0" }`) p.parseAndSetJob(data) if received == nil { t.Fatal("onJob not called") } if received.ID != "abc123" { t.Errorf("ID = %q, want abc123", received.ID) } if received.Height != 3688000 { t.Errorf("Height = %d, want 3688000", received.Height) } if received.Algo != "rx/0" { t.Errorf("Algo = %q, want rx/0", received.Algo) } if received.Target != "ffffffff" { t.Errorf("Target = %q, want ffffffff", received.Target) } } func TestParseAndSetJobSetsCurrentJob(t *testing.T) { p := newTestProxy() data := json.RawMessage(`{"job_id":"xyz","height":100,"blob":"aa","target":"ff","seed_hash":"bb"}`) p.parseAndSetJob(data) got := p.GetCurrentJob() if got == nil { t.Fatal("GetCurrentJob() returned nil after parseAndSetJob") } if got.ID != "xyz" { t.Errorf("current job ID = %q, want xyz", got.ID) } } func TestParseAndSetJobTargetDerivedFromDifficulty(t *testing.T) { p := newTestProxy() var received *Job p.SetCallbacks(func(j *Job) { received = j }, nil, nil) // No target field — should be derived from difficulty. data := json.RawMessage(`{"job_id":"d1","height":1,"blob":"aa","seed_hash":"bb","difficulty":10000}`) p.parseAndSetJob(data) if received == nil { t.Fatal("onJob not called") } if received.Target == "" { t.Error("Target should be derived from difficulty when absent") } if len(received.Target) != 64 { t.Errorf("derived Target length = %d, want 64 hex chars", len(received.Target)) } } func TestParseAndSetJobInvalidJSONIgnored(t *testing.T) { p := newTestProxy() called := false p.SetCallbacks(func(j *Job) { called = true }, nil, nil) p.parseAndSetJob(json.RawMessage(`this is not json`)) if called { t.Error("onJob should not be called for invalid JSON") } } func TestParseAndSetJobCallbackNilSafe(t *testing.T) { p := newTestProxy() // No callbacks set — must not panic. data := json.RawMessage(`{"job_id":"safe","height":1,"blob":"aa","target":"ff","seed_hash":"bb"}`) p.parseAndSetJob(data) } func TestParseAndSetJobOverwritesPrevious(t *testing.T) { p := newTestProxy() p.parseAndSetJob(json.RawMessage(`{"job_id":"first","height":1,"blob":"aa","target":"ff","seed_hash":"bb"}`)) p.parseAndSetJob(json.RawMessage(`{"job_id":"second","height":2,"blob":"cc","target":"ff","seed_hash":"bb"}`)) got := p.GetCurrentJob() if got.ID != "second" { t.Errorf("expected latest job ID 'second', got %q", got.ID) } } // ─── difficultyToTarget ─────────────────────────────────────────────────────── func TestDifficultyToTargetLength(t *testing.T) { p := newTestProxy() target := p.difficultyToTarget(100000) if len(target) != 64 { t.Errorf("target hex length = %d, want 64", len(target)) } } func TestDifficultyToTargetDiff1(t *testing.T) { p := newTestProxy() target := p.difficultyToTarget(1) // Difficulty 1 → max target (all f's). if !strings.HasPrefix(target, "ff") && target != strings.Repeat("f", 64) { // At diff=1 we get maxTarget, which reversed is all ff... // Just check it's non-zero and 64 chars long. t.Logf("diff=1 target: %s", target) } } func TestDifficultyToTargetHigherDiffLowerTarget(t *testing.T) { p := newTestProxy() t1 := p.difficultyToTarget(1000) t2 := p.difficultyToTarget(1000000) // Higher difficulty → lower target value in little-endian hex. // We can't compare directly without reversing, but they must differ. if t1 == t2 { t.Error("different difficulties should produce different targets") } } // ─── parseSubmitResult ──────────────────────────────────────────────────────── func TestParseSubmitResultNilError(t *testing.T) { resp := StratumResponse{ID: 1, Result: json.RawMessage(`true`)} accepted, msg := parseSubmitResult(resp) if !accepted || msg != "" { t.Errorf("true result: accepted=%v msg=%q", accepted, msg) } } func TestParseSubmitResultStatusOK(t *testing.T) { resp := StratumResponse{ID: 1, Result: json.RawMessage(`{"status":"OK"}`)} accepted, msg := parseSubmitResult(resp) if !accepted || msg != "" { t.Errorf("status=OK: accepted=%v msg=%q", accepted, msg) } } func TestParseSubmitResultStatusAccepted(t *testing.T) { resp := StratumResponse{ID: 1, Result: json.RawMessage(`{"status":"ACCEPTED"}`)} accepted, _ := parseSubmitResult(resp) if !accepted { t.Error("status=ACCEPTED should be accepted") } } func TestParseSubmitResultRejectedBool(t *testing.T) { resp := StratumResponse{ID: 1, Result: json.RawMessage(`false`)} accepted, _ := parseSubmitResult(resp) if accepted { t.Error("false result should be rejected") } } func TestParseSubmitResultStringError(t *testing.T) { resp := StratumResponse{ID: 1, Error: "low difficulty"} accepted, msg := parseSubmitResult(resp) if accepted || msg != "low difficulty" { t.Errorf("string error: accepted=%v msg=%q", accepted, msg) } } func TestParseSubmitResultArrayError(t *testing.T) { resp := StratumResponse{ID: 1, Error: []interface{}{float64(23), "invalid share"}} accepted, msg := parseSubmitResult(resp) if accepted || msg != "invalid share" { t.Errorf("array error: accepted=%v msg=%q", accepted, msg) } } func TestParseSubmitResultMapError(t *testing.T) { resp := StratumResponse{ID: 1, Error: map[string]interface{}{"message": "duplicate share"}} accepted, msg := parseSubmitResult(resp) if accepted || msg != "duplicate share" { t.Errorf("map error: accepted=%v msg=%q", accepted, msg) } } func TestParseSubmitResultEmptyResult(t *testing.T) { resp := StratumResponse{ID: 1} accepted, _ := parseSubmitResult(resp) // Empty result with no error → optimistically accepted. if !accepted { t.Error("empty result with no error should be accepted") } } // ─── ParseBlob ──────────────────────────────────────────────────────────────── func TestParseBlobValidLength(t *testing.T) { // 76-byte Monero-style blob (152 hex chars). blob := strings.Repeat("ab", 76) result, err := ParseBlob(blob) if err != nil { t.Fatalf("ParseBlob valid: %v", err) } if result["nonce_offset"] != 9 { t.Errorf("nonce_offset = %v, want 9", result["nonce_offset"]) } } func TestParseBlobTooShort(t *testing.T) { blob := strings.Repeat("ab", 10) // 10 bytes — too short _, err := ParseBlob(blob) if err == nil { t.Error("expected error for blob shorter than 43 bytes") } } func TestParseBlobInvalidHex(t *testing.T) { _, err := ParseBlob("not-hex") if err == nil { t.Error("expected error for non-hex input") } } // ─── FromModelJob / ToModelJob ──────────────────────────────────────────────── func TestFromModelJobNil(t *testing.T) { if j := FromModelJob(nil); j != nil { t.Error("FromModelJob(nil) should return nil") } } func TestFromModelJobRoundTrip(t *testing.T) { mj := &models.Job{ ID: "job-rt", Height: 3500000, Difficulty: 999999, BlockTemplate: "template", SeedHash: "aabbcc", Target: "ffffff", CreatedAt: time.Now(), } pj := FromModelJob(mj) if pj == nil { t.Fatal("FromModelJob returned nil") } if pj.ID != mj.ID || pj.Height != mj.Height || pj.Difficulty != mj.Difficulty { t.Errorf("FromModelJob fields mismatch: %+v", pj) } back := pj.ToModelJob() if back.ID != mj.ID || back.Height != mj.Height { t.Errorf("ToModelJob round-trip failed: %+v", back) } } func TestToModelJobSetsCreatedAt(t *testing.T) { before := time.Now() pj := &Job{ID: "ts-test", Height: 1} mj := pj.ToModelJob() if mj.CreatedAt.Before(before) { t.Error("ToModelJob.CreatedAt should be set to current time") } } // ─── GetCurrentJob nil guard ────────────────────────────────────────────────── func TestGetCurrentJobNilBeforeFirst(t *testing.T) { p := newTestProxy() if j := p.GetCurrentJob(); j != nil { t.Errorf("expected nil before first job, got %+v", j) } }