Files
AetherForge/server/internal/pool/proxy_extra_test.go

291 lines
9.0 KiB
Go

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