291 lines
9.0 KiB
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)
|
|
}
|
|
}
|