go-p2p/node/worker_test.go
Virgil 885070d241
All checks were successful
Security Scan / security (push) Successful in 9s
Test / test (push) Successful in 1m41s
refactor(node): adopt AX naming across core APIs
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-30 22:46:11 +00:00

1417 lines
40 KiB
Go

package node
import (
"encoding/base64"
"testing"
"time"
core "dappco.re/go/core"
)
func setupTestEnvironment(t *testing.T) func() {
tmpDir := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", testJoinPath(tmpDir, "config"))
t.Setenv("XDG_DATA_HOME", testJoinPath(tmpDir, "data"))
return func() {}
}
func TestWorker_NewWorker_Good(t *testing.T) {
cleanup := setupTestEnvironment(t)
defer cleanup()
dir := t.TempDir()
nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir))
if err != nil {
t.Fatalf("failed to create node manager: %v", err)
}
if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil {
t.Fatalf("failed to generate identity: %v", err)
}
pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json"))
if err != nil {
t.Fatalf("failed to create peer registry: %v", err)
}
transport := NewTransport(nm, pr, DefaultTransportConfig())
worker := NewWorker(nm, transport)
worker.DataDirectory = t.TempDir()
if worker == nil {
t.Fatal("NewWorker returned nil")
}
if worker.nodeManager != nm {
t.Error("worker.nodeManager not set correctly")
}
if worker.transport != transport {
t.Error("worker.transport not set correctly")
}
}
func TestWorker_SetMinerManager_Good(t *testing.T) {
cleanup := setupTestEnvironment(t)
defer cleanup()
dir := t.TempDir()
nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir))
if err != nil {
t.Fatalf("failed to create node manager: %v", err)
}
if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil {
t.Fatalf("failed to generate identity: %v", err)
}
pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json"))
if err != nil {
t.Fatalf("failed to create peer registry: %v", err)
}
transport := NewTransport(nm, pr, DefaultTransportConfig())
worker := NewWorker(nm, transport)
worker.DataDirectory = t.TempDir()
mockManager := &mockMinerManager{}
worker.SetMinerManager(mockManager)
if worker.minerManager != mockManager {
t.Error("minerManager not set correctly")
}
}
func TestWorker_SetProfileManager_Good(t *testing.T) {
cleanup := setupTestEnvironment(t)
defer cleanup()
dir := t.TempDir()
nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir))
if err != nil {
t.Fatalf("failed to create node manager: %v", err)
}
if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil {
t.Fatalf("failed to generate identity: %v", err)
}
pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json"))
if err != nil {
t.Fatalf("failed to create peer registry: %v", err)
}
transport := NewTransport(nm, pr, DefaultTransportConfig())
worker := NewWorker(nm, transport)
worker.DataDirectory = t.TempDir()
mockProfile := &mockProfileManager{}
worker.SetProfileManager(mockProfile)
if worker.profileManager != mockProfile {
t.Error("profileManager not set correctly")
}
}
func TestWorker_HandlePing_Good(t *testing.T) {
cleanup := setupTestEnvironment(t)
defer cleanup()
dir := t.TempDir()
nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir))
if err != nil {
t.Fatalf("failed to create node manager: %v", err)
}
if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil {
t.Fatalf("failed to generate identity: %v", err)
}
pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json"))
if err != nil {
t.Fatalf("failed to create peer registry: %v", err)
}
transport := NewTransport(nm, pr, DefaultTransportConfig())
worker := NewWorker(nm, transport)
worker.DataDirectory = t.TempDir()
// Create a ping message
identity := nm.Identity()
if identity == nil {
t.Fatal("expected identity to be generated")
}
pingPayload := PingPayload{SentAt: time.Now().UnixMilli()}
pingMsg, err := NewMessage(MessagePing, "sender-id", identity.ID, pingPayload)
if err != nil {
t.Fatalf("failed to create ping message: %v", err)
}
// Call handlePing directly
response, err := worker.handlePing(pingMsg)
if err != nil {
t.Fatalf("handlePing returned error: %v", err)
}
if response == nil {
t.Fatal("handlePing returned nil response")
}
if response.Type != MessagePong {
t.Errorf("expected response type %s, got %s", MessagePong, response.Type)
}
var pong PongPayload
if err := response.ParsePayload(&pong); err != nil {
t.Fatalf("failed to parse pong payload: %v", err)
}
if pong.SentAt != pingPayload.SentAt {
t.Errorf("pong SentAt mismatch: expected %d, got %d", pingPayload.SentAt, pong.SentAt)
}
if pong.ReceivedAt == 0 {
t.Error("pong ReceivedAt not set")
}
}
func TestWorker_HandleStats_Good(t *testing.T) {
cleanup := setupTestEnvironment(t)
defer cleanup()
dir := t.TempDir()
nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir))
if err != nil {
t.Fatalf("failed to create node manager: %v", err)
}
if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil {
t.Fatalf("failed to generate identity: %v", err)
}
pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json"))
if err != nil {
t.Fatalf("failed to create peer registry: %v", err)
}
transport := NewTransport(nm, pr, DefaultTransportConfig())
worker := NewWorker(nm, transport)
worker.DataDirectory = t.TempDir()
// Create a stats request message.
identity := nm.Identity()
if identity == nil {
t.Fatal("expected identity to be generated")
}
msg, err := NewMessage(MessageGetStats, "sender-id", identity.ID, nil)
if err != nil {
t.Fatalf("failed to create stats request message: %v", err)
}
// Call handleStats directly (without miner manager).
response, err := worker.handleStats(msg)
if err != nil {
t.Fatalf("handleStats returned error: %v", err)
}
if response == nil {
t.Fatal("handleStats returned nil response")
}
if response.Type != MessageStats {
t.Errorf("expected response type %s, got %s", MessageStats, response.Type)
}
var stats StatsPayload
if err := response.ParsePayload(&stats); err != nil {
t.Fatalf("failed to parse stats payload: %v", err)
}
if stats.NodeID != identity.ID {
t.Errorf("stats NodeID mismatch: expected %s, got %s", identity.ID, stats.NodeID)
}
if stats.NodeName != identity.Name {
t.Errorf("stats NodeName mismatch: expected %s, got %s", identity.Name, stats.NodeName)
}
}
func TestWorker_HandleStartMiner_NoManager_Bad(t *testing.T) {
cleanup := setupTestEnvironment(t)
defer cleanup()
dir := t.TempDir()
nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir))
if err != nil {
t.Fatalf("failed to create node manager: %v", err)
}
if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil {
t.Fatalf("failed to generate identity: %v", err)
}
pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json"))
if err != nil {
t.Fatalf("failed to create peer registry: %v", err)
}
transport := NewTransport(nm, pr, DefaultTransportConfig())
worker := NewWorker(nm, transport)
worker.DataDirectory = t.TempDir()
// Create a start_miner message
identity := nm.Identity()
if identity == nil {
t.Fatal("expected identity to be generated")
}
payload := StartMinerPayload{MinerType: "xmrig", ProfileID: "test-profile"}
msg, err := NewMessage(MessageStartMiner, "sender-id", identity.ID, payload)
if err != nil {
t.Fatalf("failed to create start_miner message: %v", err)
}
// Without miner manager, should return error
_, err = worker.handleStartMiner(msg)
if err == nil {
t.Error("expected error when miner manager is nil")
}
}
func TestWorker_HandleStopMiner_NoManager_Bad(t *testing.T) {
cleanup := setupTestEnvironment(t)
defer cleanup()
dir := t.TempDir()
nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir))
if err != nil {
t.Fatalf("failed to create node manager: %v", err)
}
if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil {
t.Fatalf("failed to generate identity: %v", err)
}
pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json"))
if err != nil {
t.Fatalf("failed to create peer registry: %v", err)
}
transport := NewTransport(nm, pr, DefaultTransportConfig())
worker := NewWorker(nm, transport)
worker.DataDirectory = t.TempDir()
// Create a stop_miner message
identity := nm.Identity()
if identity == nil {
t.Fatal("expected identity to be generated")
}
payload := StopMinerPayload{MinerName: "test-miner"}
msg, err := NewMessage(MessageStopMiner, "sender-id", identity.ID, payload)
if err != nil {
t.Fatalf("failed to create stop_miner message: %v", err)
}
// Without miner manager, should return error
_, err = worker.handleStopMiner(msg)
if err == nil {
t.Error("expected error when miner manager is nil")
}
}
func TestWorker_HandleLogs_NoManager_Bad(t *testing.T) {
cleanup := setupTestEnvironment(t)
defer cleanup()
dir := t.TempDir()
nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir))
if err != nil {
t.Fatalf("failed to create node manager: %v", err)
}
if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil {
t.Fatalf("failed to generate identity: %v", err)
}
pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json"))
if err != nil {
t.Fatalf("failed to create peer registry: %v", err)
}
transport := NewTransport(nm, pr, DefaultTransportConfig())
worker := NewWorker(nm, transport)
worker.DataDirectory = t.TempDir()
// Create a logs request message.
identity := nm.Identity()
if identity == nil {
t.Fatal("expected identity to be generated")
}
payload := LogsRequestPayload{MinerName: "test-miner", Lines: 100}
msg, err := NewMessage(MessageGetLogs, "sender-id", identity.ID, payload)
if err != nil {
t.Fatalf("failed to create logs request message: %v", err)
}
// Without miner manager, should return error
_, err = worker.handleLogs(msg)
if err == nil {
t.Error("expected error when miner manager is nil")
}
}
func TestWorker_HandleDeploy_Profile_Good(t *testing.T) {
cleanup := setupTestEnvironment(t)
defer cleanup()
dir := t.TempDir()
nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir))
if err != nil {
t.Fatalf("failed to create node manager: %v", err)
}
if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil {
t.Fatalf("failed to generate identity: %v", err)
}
pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json"))
if err != nil {
t.Fatalf("failed to create peer registry: %v", err)
}
transport := NewTransport(nm, pr, DefaultTransportConfig())
worker := NewWorker(nm, transport)
worker.DataDirectory = t.TempDir()
// Create a deploy message for profile
identity := nm.Identity()
if identity == nil {
t.Fatal("expected identity to be generated")
}
payload := DeployPayload{
BundleType: "profile",
Data: []byte(`{"id": "test", "name": "Test Profile"}`),
Name: "test-profile",
}
msg, err := NewMessage(MessageDeploy, "sender-id", identity.ID, payload)
if err != nil {
t.Fatalf("failed to create deploy message: %v", err)
}
// Without profile manager, should return error
_, err = worker.handleDeploy(nil, msg)
if err == nil {
t.Error("expected error when profile manager is nil")
}
}
func TestWorker_HandleDeploy_UnknownType_Bad(t *testing.T) {
cleanup := setupTestEnvironment(t)
defer cleanup()
dir := t.TempDir()
nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir))
if err != nil {
t.Fatalf("failed to create node manager: %v", err)
}
if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil {
t.Fatalf("failed to generate identity: %v", err)
}
pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json"))
if err != nil {
t.Fatalf("failed to create peer registry: %v", err)
}
transport := NewTransport(nm, pr, DefaultTransportConfig())
worker := NewWorker(nm, transport)
worker.DataDirectory = t.TempDir()
// Create a deploy message with unknown type
identity := nm.Identity()
if identity == nil {
t.Fatal("expected identity to be generated")
}
payload := DeployPayload{
BundleType: "unknown",
Data: []byte(`{}`),
Name: "test",
}
msg, err := NewMessage(MessageDeploy, "sender-id", identity.ID, payload)
if err != nil {
t.Fatalf("failed to create deploy message: %v", err)
}
_, err = worker.handleDeploy(nil, msg)
if err == nil {
t.Error("expected error for unknown bundle type")
}
}
func TestWorker_ConvertMinerStats_Good(t *testing.T) {
tests := []struct {
name string
rawStats any
wantHash float64
}{
{
name: "MapWithHashrate",
rawStats: map[string]any{
"hashrate": 100.5,
"shares": 10,
"rejected": 2,
"uptime": 3600,
"pool": "test-pool",
"algorithm": "rx/0",
},
wantHash: 100.5,
},
{
name: "EmptyMap",
rawStats: map[string]any{},
wantHash: 0,
},
{
name: "NonMap",
rawStats: "not a map",
wantHash: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mock := &mockMinerInstance{name: "test", minerType: "xmrig"}
result := convertMinerStats(mock, tt.rawStats)
if result.Name != "test" {
t.Errorf("expected name 'test', got '%s'", result.Name)
}
if result.Hashrate != tt.wantHash {
t.Errorf("expected hashrate %f, got %f", tt.wantHash, result.Hashrate)
}
})
}
}
// Mock implementations for testing
type mockMinerManager struct {
miners []MinerInstance
}
func (m *mockMinerManager) StartMiner(minerType string, config any) (MinerInstance, error) {
return nil, nil
}
func (m *mockMinerManager) StopMiner(name string) error {
return nil
}
func (m *mockMinerManager) ListMiners() []MinerInstance {
return m.miners
}
func (m *mockMinerManager) GetMiner(name string) (MinerInstance, error) {
for _, miner := range m.miners {
if miner.GetName() == name {
return miner, nil
}
}
return nil, nil
}
func (m *mockMinerManager) Miner(name string) (MinerInstance, error) {
return m.GetMiner(name)
}
type mockMinerInstance struct {
name string
minerType string
stats any
}
func (m *mockMinerInstance) GetName() string { return m.name }
func (m *mockMinerInstance) GetType() string { return m.minerType }
func (m *mockMinerInstance) GetStats() (any, error) { return m.stats, nil }
func (m *mockMinerInstance) GetConsoleHistory(lines int) []string { return []string{} }
func (m *mockMinerInstance) Name() string { return m.GetName() }
func (m *mockMinerInstance) Type() string { return m.GetType() }
func (m *mockMinerInstance) Stats() (any, error) { return m.GetStats() }
func (m *mockMinerInstance) ConsoleHistory(lines int) []string { return m.GetConsoleHistory(lines) }
type mockProfileManager struct{}
func (m *mockProfileManager) GetProfile(id string) (any, error) {
return nil, nil
}
func (m *mockProfileManager) Profile(id string) (any, error) {
return m.GetProfile(id)
}
func (m *mockProfileManager) SaveProfile(profile any) error {
return nil
}
// --- Enhanced worker handler tests for full code path coverage ---
// mockMinerManagerFailing always returns errors from StartMiner.
type mockMinerManagerFailing struct {
mockMinerManager
}
func (m *mockMinerManagerFailing) StartMiner(minerType string, config any) (MinerInstance, error) {
return nil, core.E("mockMinerManagerFailing.StartMiner", "mining hardware not available", nil)
}
func (m *mockMinerManagerFailing) StopMiner(name string) error {
return core.E("mockMinerManagerFailing.StopMiner", "miner "+name+" not found", nil)
}
func (m *mockMinerManagerFailing) GetMiner(name string) (MinerInstance, error) {
return nil, core.E("mockMinerManagerFailing.Miner", "miner "+name+" not found", nil)
}
func (m *mockMinerManagerFailing) Miner(name string) (MinerInstance, error) {
return m.GetMiner(name)
}
// mockProfileManagerFull implements ProfileManager that returns real data.
type mockProfileManagerFull struct {
profiles map[string]any
}
func (m *mockProfileManagerFull) GetProfile(id string) (any, error) {
p, ok := m.profiles[id]
if !ok {
return nil, core.E("mockProfileManagerFull.GetProfile", "profile "+id+" not found", nil)
}
return p, nil
}
func (m *mockProfileManagerFull) SaveProfile(profile any) error {
return nil
}
func (m *mockProfileManagerFull) Profile(id string) (any, error) {
return m.GetProfile(id)
}
// mockProfileManagerFailing always returns errors.
type mockProfileManagerFailing struct{}
func (m *mockProfileManagerFailing) GetProfile(id string) (any, error) {
return nil, core.E("mockProfileManagerFailing.GetProfile", "profile "+id+" not found", nil)
}
func (m *mockProfileManagerFailing) SaveProfile(profile any) error {
return core.E("mockProfileManagerFailing.SaveProfile", "save failed", nil)
}
func (m *mockProfileManagerFailing) Profile(id string) (any, error) {
return m.GetProfile(id)
}
func TestWorker_HandleStartMiner_WithManager_Good(t *testing.T) {
cleanup := setupTestEnvironment(t)
defer cleanup()
dir := t.TempDir()
nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir))
if err != nil {
t.Fatalf("failed to create node manager: %v", err)
}
if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil {
t.Fatalf("failed to generate identity: %v", err)
}
pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json"))
if err != nil {
t.Fatalf("failed to create peer registry: %v", err)
}
transport := NewTransport(nm, pr, DefaultTransportConfig())
worker := NewWorker(nm, transport)
worker.DataDirectory = t.TempDir()
mm := &mockMinerManager{
miners: []MinerInstance{},
}
// Override StartMiner to return a real instance
mmFull := &mockMinerManagerWithStart{}
worker.SetMinerManager(mmFull)
identity := nm.Identity()
t.Run("WithConfigOverride", func(t *testing.T) {
payload := StartMinerPayload{
MinerType: "xmrig",
Config: RawMessage(`{"pool":"test:3333"}`),
}
msg, err := NewMessage(MessageStartMiner, "sender-id", identity.ID, payload)
if err != nil {
t.Fatalf("failed to create message: %v", err)
}
response, err := worker.handleStartMiner(msg)
if err != nil {
t.Fatalf("handleStartMiner returned error: %v", err)
}
if response.Type != MessageMinerAck {
t.Errorf("expected type %s, got %s", MessageMinerAck, response.Type)
}
var ack MinerAckPayload
if err := response.ParsePayload(&ack); err != nil {
t.Fatalf("failed to parse ack: %v", err)
}
if !ack.Success {
t.Errorf("expected success, got error: %s", ack.Error)
}
if ack.MinerName == "" {
t.Error("expected miner name in ack")
}
})
t.Run("EmptyMinerType", func(t *testing.T) {
payload := StartMinerPayload{
MinerType: "",
Config: RawMessage(`{}`),
}
msg, err := NewMessage(MessageStartMiner, "sender-id", identity.ID, payload)
if err != nil {
t.Fatalf("failed to create message: %v", err)
}
_, err = worker.handleStartMiner(msg)
if err == nil {
t.Error("expected error for empty miner type")
}
})
t.Run("WithProfileManager", func(t *testing.T) {
pm := &mockProfileManagerFull{
profiles: map[string]any{
"test-profile": map[string]any{"pool": "pool.test:3333"},
},
}
worker.SetProfileManager(pm)
payload := StartMinerPayload{
MinerType: "xmrig",
ProfileID: "test-profile",
}
msg, err := NewMessage(MessageStartMiner, "sender-id", identity.ID, payload)
if err != nil {
t.Fatalf("failed to create message: %v", err)
}
response, err := worker.handleStartMiner(msg)
if err != nil {
t.Fatalf("handleStartMiner returned error: %v", err)
}
var ack MinerAckPayload
response.ParsePayload(&ack)
if !ack.Success {
t.Errorf("expected success, got error: %s", ack.Error)
}
})
t.Run("ProfileNotFound", func(t *testing.T) {
pm := &mockProfileManagerFailing{}
worker.SetProfileManager(pm)
payload := StartMinerPayload{
MinerType: "xmrig",
ProfileID: "missing-profile",
}
msg, err := NewMessage(MessageStartMiner, "sender-id", identity.ID, payload)
if err != nil {
t.Fatalf("failed to create message: %v", err)
}
_, err = worker.handleStartMiner(msg)
if err == nil {
t.Error("expected error for missing profile")
}
})
t.Run("StartFailsReturnsAck", func(t *testing.T) {
worker.SetMinerManager(&mockMinerManagerFailing{})
worker.SetProfileManager(nil)
payload := StartMinerPayload{
MinerType: "xmrig",
Config: RawMessage(`{}`),
}
msg, err := NewMessage(MessageStartMiner, "sender-id", identity.ID, payload)
if err != nil {
t.Fatalf("failed to create message: %v", err)
}
response, err := worker.handleStartMiner(msg)
if err != nil {
t.Fatalf("handleStartMiner should not return error when start fails: %v", err)
}
var ack MinerAckPayload
response.ParsePayload(&ack)
if ack.Success {
t.Error("expected failure ack")
}
if ack.Error == "" {
t.Error("expected error message in ack")
}
})
_ = mm // suppress lint
}
// mockMinerManagerWithStart returns real instances from StartMiner.
type mockMinerManagerWithStart struct {
mockMinerManager
counter int
}
func (m *mockMinerManagerWithStart) StartMiner(minerType string, config any) (MinerInstance, error) {
m.counter++
name := core.Sprintf("%s-%d", minerType, m.counter)
return &mockMinerInstance{name: name, minerType: minerType}, nil
}
func TestWorker_HandleStopMiner_WithManager_Good(t *testing.T) {
cleanup := setupTestEnvironment(t)
defer cleanup()
dir := t.TempDir()
nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir))
if err != nil {
t.Fatalf("failed to create node manager: %v", err)
}
if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil {
t.Fatalf("failed to generate identity: %v", err)
}
pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json"))
if err != nil {
t.Fatalf("failed to create peer registry: %v", err)
}
transport := NewTransport(nm, pr, DefaultTransportConfig())
worker := NewWorker(nm, transport)
worker.DataDirectory = t.TempDir()
identity := nm.Identity()
t.Run("Success", func(t *testing.T) {
worker.SetMinerManager(&mockMinerManager{})
payload := StopMinerPayload{MinerName: "test-miner"}
msg, _ := NewMessage(MessageStopMiner, "sender-id", identity.ID, payload)
response, err := worker.handleStopMiner(msg)
if err != nil {
t.Fatalf("handleStopMiner returned error: %v", err)
}
var ack MinerAckPayload
response.ParsePayload(&ack)
if !ack.Success {
t.Errorf("expected success, got error: %s", ack.Error)
}
if ack.MinerName != "test-miner" {
t.Errorf("expected miner name 'test-miner', got '%s'", ack.MinerName)
}
})
t.Run("StopFails", func(t *testing.T) {
worker.SetMinerManager(&mockMinerManagerFailing{})
payload := StopMinerPayload{MinerName: "missing-miner"}
msg, _ := NewMessage(MessageStopMiner, "sender-id", identity.ID, payload)
response, err := worker.handleStopMiner(msg)
if err != nil {
t.Fatalf("handleStopMiner should not return error: %v", err)
}
var ack MinerAckPayload
response.ParsePayload(&ack)
if ack.Success {
t.Error("expected failure ack")
}
if ack.Error == "" {
t.Error("expected error message in ack")
}
})
}
func TestWorker_HandleLogs_WithManager_Good(t *testing.T) {
cleanup := setupTestEnvironment(t)
defer cleanup()
dir := t.TempDir()
nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir))
if err != nil {
t.Fatalf("failed to create node manager: %v", err)
}
if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil {
t.Fatalf("failed to generate identity: %v", err)
}
pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json"))
if err != nil {
t.Fatalf("failed to create peer registry: %v", err)
}
transport := NewTransport(nm, pr, DefaultTransportConfig())
worker := NewWorker(nm, transport)
worker.DataDirectory = t.TempDir()
identity := nm.Identity()
t.Run("Success", func(t *testing.T) {
mm := &mockMinerManager{
miners: []MinerInstance{
&mockMinerInstance{
name: "test-miner",
minerType: "xmrig",
},
},
}
worker.SetMinerManager(mm)
payload := LogsRequestPayload{MinerName: "test-miner", Lines: 100}
msg, _ := NewMessage(MessageGetLogs, "sender-id", identity.ID, payload)
response, err := worker.handleLogs(msg)
if err != nil {
t.Fatalf("handleLogs returned error: %v", err)
}
if response.Type != MessageLogs {
t.Errorf("expected type %s, got %s", MessageLogs, response.Type)
}
var logs LogsPayload
response.ParsePayload(&logs)
if logs.MinerName != "test-miner" {
t.Errorf("expected miner name 'test-miner', got '%s'", logs.MinerName)
}
})
t.Run("MinerNotFound", func(t *testing.T) {
// Use a manager that returns an error when the miner is missing.
mm := &mockMinerManagerFailing{}
worker.SetMinerManager(mm)
payload := LogsRequestPayload{MinerName: "non-existent", Lines: 50}
msg, _ := NewMessage(MessageGetLogs, "sender-id", identity.ID, payload)
_, err := worker.handleLogs(msg)
if err == nil {
t.Error("expected error for non-existent miner")
}
})
t.Run("NegativeLines", func(t *testing.T) {
mm := &mockMinerManager{
miners: []MinerInstance{
&mockMinerInstance{name: "test-miner", minerType: "xmrig"},
},
}
worker.SetMinerManager(mm)
payload := LogsRequestPayload{MinerName: "test-miner", Lines: -1}
msg, _ := NewMessage(MessageGetLogs, "sender-id", identity.ID, payload)
response, err := worker.handleLogs(msg)
if err != nil {
t.Fatalf("handleLogs returned error: %v", err)
}
// Lines <= 0 should be clamped to maxLogLines
if response.Type != MessageLogs {
t.Errorf("expected %s, got %s", MessageLogs, response.Type)
}
})
t.Run("ExcessiveLines", func(t *testing.T) {
mm := &mockMinerManager{
miners: []MinerInstance{
&mockMinerInstance{name: "test-miner", minerType: "xmrig"},
},
}
worker.SetMinerManager(mm)
payload := LogsRequestPayload{MinerName: "test-miner", Lines: 999999}
msg, _ := NewMessage(MessageGetLogs, "sender-id", identity.ID, payload)
response, err := worker.handleLogs(msg)
if err != nil {
t.Fatalf("handleLogs returned error: %v", err)
}
if response.Type != MessageLogs {
t.Errorf("expected %s, got %s", MessageLogs, response.Type)
}
})
}
func TestWorker_HandleStats_WithMinerManager_Good(t *testing.T) {
cleanup := setupTestEnvironment(t)
defer cleanup()
dir := t.TempDir()
nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir))
if err != nil {
t.Fatalf("failed to create node manager: %v", err)
}
if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil {
t.Fatalf("failed to generate identity: %v", err)
}
pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json"))
if err != nil {
t.Fatalf("failed to create peer registry: %v", err)
}
transport := NewTransport(nm, pr, DefaultTransportConfig())
worker := NewWorker(nm, transport)
worker.DataDirectory = t.TempDir()
identity := nm.Identity()
// Set miner manager with miners that have real stats
mm := &mockMinerManager{
miners: []MinerInstance{
&mockMinerInstance{
name: "miner-1",
minerType: "xmrig",
stats: map[string]any{
"hashrate": 500.0,
"shares": 25,
"rejected": 1,
"uptime": 3600,
"pool": "pool.test:3333",
"algorithm": "rx/0",
},
},
&mockMinerInstance{
name: "miner-2",
minerType: "tt-miner",
stats: map[string]any{
"hashrate": 1200.0,
},
},
},
}
worker.SetMinerManager(mm)
msg, _ := NewMessage(MessageGetStats, "sender-id", identity.ID, nil)
response, err := worker.handleStats(msg)
if err != nil {
t.Fatalf("handleStats returned error: %v", err)
}
var stats StatsPayload
response.ParsePayload(&stats)
if len(stats.Miners) != 2 {
t.Errorf("expected 2 miners in stats, got %d", len(stats.Miners))
}
}
func TestWorker_HandleMessage_UnknownType_Bad(t *testing.T) {
cleanup := setupTestEnvironment(t)
defer cleanup()
dir := t.TempDir()
nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir))
if err != nil {
t.Fatalf("failed to create node manager: %v", err)
}
if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil {
t.Fatalf("failed to generate identity: %v", err)
}
pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json"))
if err != nil {
t.Fatalf("failed to create peer registry: %v", err)
}
transport := NewTransport(nm, pr, DefaultTransportConfig())
worker := NewWorker(nm, transport)
worker.DataDirectory = t.TempDir()
identity := nm.Identity()
msg, _ := NewMessage("unknown_type", "sender-id", identity.ID, nil)
// HandleMessage with unknown type should return silently (no panic)
worker.HandleMessage(nil, msg)
}
func TestWorker_HandleDeploy_ProfileWithManager_Good(t *testing.T) {
cleanup := setupTestEnvironment(t)
defer cleanup()
dir := t.TempDir()
nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir))
if err != nil {
t.Fatalf("failed to create node manager: %v", err)
}
if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil {
t.Fatalf("failed to generate identity: %v", err)
}
pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json"))
if err != nil {
t.Fatalf("failed to create peer registry: %v", err)
}
transport := NewTransport(nm, pr, DefaultTransportConfig())
worker := NewWorker(nm, transport)
worker.DataDirectory = t.TempDir()
pm := &mockProfileManagerFull{profiles: make(map[string]any)}
worker.SetProfileManager(pm)
identity := nm.Identity()
// Create an unencrypted profile bundle for deploy
profileJSON := []byte(`{"id": "deploy-test", "name": "Test Profile"}`)
bundle, err := CreateProfileBundleUnencrypted(profileJSON, "deploy-test")
if err != nil {
t.Fatalf("failed to create bundle: %v", err)
}
payload := DeployPayload{
BundleType: string(BundleProfile),
Data: bundle.Data,
Checksum: bundle.Checksum,
Name: "deploy-test",
}
msg, _ := NewMessage(MessageDeploy, "sender-id", identity.ID, payload)
response, err := worker.handleDeploy(nil, msg)
if err != nil {
t.Fatalf("handleDeploy returned error: %v", err)
}
var ack DeployAckPayload
response.ParsePayload(&ack)
if !ack.Success {
t.Errorf("expected success, got error: %s", ack.Error)
}
if ack.Name != "deploy-test" {
t.Errorf("expected name 'deploy-test', got '%s'", ack.Name)
}
}
func TestWorker_HandleDeploy_ProfileSaveFails_Bad(t *testing.T) {
cleanup := setupTestEnvironment(t)
defer cleanup()
dir := t.TempDir()
nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir))
if err != nil {
t.Fatalf("failed to create node manager: %v", err)
}
if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil {
t.Fatalf("failed to generate identity: %v", err)
}
pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json"))
if err != nil {
t.Fatalf("failed to create peer registry: %v", err)
}
transport := NewTransport(nm, pr, DefaultTransportConfig())
worker := NewWorker(nm, transport)
worker.DataDirectory = t.TempDir()
worker.SetProfileManager(&mockProfileManagerFailing{})
identity := nm.Identity()
profileJSON := []byte(`{"id": "fail-test"}`)
bundle, _ := CreateProfileBundleUnencrypted(profileJSON, "fail-test")
payload := DeployPayload{
BundleType: string(BundleProfile),
Data: bundle.Data,
Checksum: bundle.Checksum,
Name: "fail-test",
}
msg, _ := NewMessage(MessageDeploy, "sender-id", identity.ID, payload)
response, err := worker.handleDeploy(nil, msg)
if err != nil {
t.Fatalf("handleDeploy should return ack with error, not error: %v", err)
}
var ack DeployAckPayload
response.ParsePayload(&ack)
if ack.Success {
t.Error("expected failure ack when save fails")
}
}
func TestWorker_HandleDeploy_MinerBundle_Good(t *testing.T) {
cleanup := setupTestEnvironment(t)
defer cleanup()
dir := t.TempDir()
nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir))
if err != nil {
t.Fatalf("failed to create node manager: %v", err)
}
if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil {
t.Fatalf("failed to generate identity: %v", err)
}
pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json"))
if err != nil {
t.Fatalf("failed to create peer registry: %v", err)
}
transport := NewTransport(nm, pr, DefaultTransportConfig())
worker := NewWorker(nm, transport)
worker.DataDirectory = t.TempDir()
pm := &mockProfileManagerFull{profiles: make(map[string]any)}
worker.SetProfileManager(pm)
identity := nm.Identity()
tmpDir := t.TempDir()
minerPath := testJoinPath(tmpDir, "test-miner")
testWriteFile(t, minerPath, []byte("fake miner binary"), 0o755)
profileJSON := []byte(`{"pool":"test:3333"}`)
// The handler extracts password as base64(conn.SharedSecret).
// Create bundle with matching password.
sharedSecret := []byte("shared-secret-32")
bundlePassword := base64.StdEncoding.EncodeToString(sharedSecret)
bundle, err := CreateMinerBundle(minerPath, profileJSON, "deploy-miner", bundlePassword)
if err != nil {
t.Fatalf("failed to create miner bundle: %v", err)
}
payload := DeployPayload{
BundleType: string(BundleMiner),
Data: bundle.Data,
Checksum: bundle.Checksum,
Name: "deploy-miner",
}
msg, _ := NewMessage(MessageDeploy, "sender-id", identity.ID, payload)
conn := &PeerConnection{
SharedSecret: sharedSecret,
}
response, err := worker.handleDeploy(conn, msg)
if err != nil {
t.Fatalf("handleDeploy returned error: %v", err)
}
var ack DeployAckPayload
response.ParsePayload(&ack)
if !ack.Success {
t.Errorf("expected success, got error: %s", ack.Error)
}
}
func TestWorker_HandleDeploy_FullBundle_Good(t *testing.T) {
cleanup := setupTestEnvironment(t)
defer cleanup()
dir := t.TempDir()
nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir))
if err != nil {
t.Fatalf("failed to create node manager: %v", err)
}
if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil {
t.Fatalf("failed to generate identity: %v", err)
}
pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json"))
if err != nil {
t.Fatalf("failed to create peer registry: %v", err)
}
transport := NewTransport(nm, pr, DefaultTransportConfig())
worker := NewWorker(nm, transport)
worker.DataDirectory = t.TempDir()
identity := nm.Identity()
tmpDir := t.TempDir()
minerPath := testJoinPath(tmpDir, "test-miner")
testWriteFile(t, minerPath, []byte("miner binary"), 0o755)
sharedSecret := []byte("full-secret-key!")
bundlePassword := base64.StdEncoding.EncodeToString(sharedSecret)
bundle, err := CreateMinerBundle(minerPath, nil, "full-deploy", bundlePassword)
if err != nil {
t.Fatalf("failed to create miner bundle: %v", err)
}
payload := DeployPayload{
BundleType: string(BundleFull),
Data: bundle.Data,
Checksum: bundle.Checksum,
Name: "full-deploy",
}
msg, _ := NewMessage(MessageDeploy, "sender-id", identity.ID, payload)
conn := &PeerConnection{SharedSecret: sharedSecret}
response, err := worker.handleDeploy(conn, msg)
if err != nil {
t.Fatalf("handleDeploy for full bundle returned error: %v", err)
}
var ack DeployAckPayload
response.ParsePayload(&ack)
if !ack.Success {
t.Errorf("expected success, got error: %s", ack.Error)
}
}
func TestWorker_HandleDeploy_MinerBundle_WithProfileManager_Good(t *testing.T) {
cleanup := setupTestEnvironment(t)
defer cleanup()
dir := t.TempDir()
nm, err := NewNodeManagerFromPaths(testNodeManagerPaths(dir))
if err != nil {
t.Fatalf("failed to create node manager: %v", err)
}
if err := nm.GenerateIdentity("test-worker", RoleWorker); err != nil {
t.Fatalf("failed to generate identity: %v", err)
}
pr, err := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json"))
if err != nil {
t.Fatalf("failed to create peer registry: %v", err)
}
transport := NewTransport(nm, pr, DefaultTransportConfig())
worker := NewWorker(nm, transport)
worker.DataDirectory = t.TempDir()
// Set a failing profile manager to exercise the warn-and-continue path
worker.SetProfileManager(&mockProfileManagerFailing{})
identity := nm.Identity()
tmpDir := t.TempDir()
minerPath := testJoinPath(tmpDir, "test-miner")
testWriteFile(t, minerPath, []byte("miner binary"), 0o755)
profileJSON := []byte(`{"pool":"test:3333"}`)
sharedSecret := []byte("profile-secret!!")
bundlePassword := base64.StdEncoding.EncodeToString(sharedSecret)
bundle, err := CreateMinerBundle(minerPath, profileJSON, "deploy-with-profile", bundlePassword)
if err != nil {
t.Fatalf("failed to create miner bundle: %v", err)
}
payload := DeployPayload{
BundleType: string(BundleMiner),
Data: bundle.Data,
Checksum: bundle.Checksum,
Name: "deploy-with-profile",
}
msg, _ := NewMessage(MessageDeploy, "sender-id", identity.ID, payload)
conn := &PeerConnection{SharedSecret: sharedSecret}
response, err := worker.handleDeploy(conn, msg)
if err != nil {
t.Fatalf("handleDeploy returned error: %v", err)
}
var ack DeployAckPayload
response.ParsePayload(&ack)
// Deploy should still succeed even if profile save fails
if !ack.Success {
t.Errorf("expected success despite profile save failure, got: %s", ack.Error)
}
}
func TestWorker_HandleDeploy_InvalidPayload_Bad(t *testing.T) {
cleanup := setupTestEnvironment(t)
defer cleanup()
dir := t.TempDir()
nm, _ := NewNodeManagerFromPaths(testNodeManagerPaths(dir))
nm.GenerateIdentity("test", RoleWorker)
pr, _ := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json"))
transport := NewTransport(nm, pr, DefaultTransportConfig())
worker := NewWorker(nm, transport)
worker.DataDirectory = t.TempDir()
identity := nm.Identity()
// Create a message with invalid payload
msg, _ := NewMessage(MessageDeploy, "sender-id", identity.ID, "invalid-payload-not-struct")
_, err := worker.handleDeploy(nil, msg)
if err == nil {
t.Error("expected error for invalid deploy payload")
}
}
func TestWorker_HandleStats_NoIdentity_Bad(t *testing.T) {
cleanup := setupTestEnvironment(t)
defer cleanup()
tmpDir := t.TempDir()
nm, _ := NewNodeManagerFromPaths(
testJoinPath(tmpDir, "priv.key"),
testJoinPath(tmpDir, "node.json"),
)
// Don't generate identity
pr, _ := NewPeerRegistryFromPath(testJoinPath(t.TempDir(), "peers.json"))
transport := NewTransport(nm, pr, DefaultTransportConfig())
worker := NewWorker(nm, transport)
worker.DataDirectory = t.TempDir()
msg, _ := NewMessage(MessageGetStats, "sender-id", "target-id", nil)
_, err := worker.handleStats(msg)
if err == nil {
t.Error("expected error when identity is not initialized")
}
}
func TestWorker_HandleMessage_IntegrationViaWebSocket_Good(t *testing.T) {
// Test HandleMessage through real WebSocket -- exercises error response sending path
tp := setupTestTransportPair(t)
worker := NewWorker(tp.ServerNode, tp.Server)
// No miner manager set -- start_miner will fail and send error response
worker.RegisterOnTransport()
controller := NewController(tp.ClientNode, tp.ClientReg, tp.Client)
tp.connectClient(t)
time.Sleep(50 * time.Millisecond)
serverID := tp.ServerNode.Identity().ID
// Send start_miner which will fail because no manager is set.
// The worker should send an error response via the connection.
err := controller.StartRemoteMiner(serverID, "xmrig", "", RawMessage(`{}`))
// Should get an error back (either protocol error or operation failed)
if err == nil {
t.Error("expected error when worker has no miner manager")
}
}
func TestWorker_HandleMessage_Stats_IntegrationViaWebSocket_Good(t *testing.T) {
// HandleMessage dispatch for stats through real WebSocket.
tp := setupTestTransportPair(t)
worker := NewWorker(tp.ServerNode, tp.Server)
mm := &mockMinerManager{
miners: []MinerInstance{
&mockMinerInstance{
name: "test-miner",
minerType: "xmrig",
stats: map[string]any{
"hashrate": 500.0,
"shares": 25,
"rejected": 1,
"uptime": 3600,
"pool": "pool.test:3333",
"algorithm": "rx/0",
},
},
},
}
worker.SetMinerManager(mm)
worker.RegisterOnTransport()
controller := NewController(tp.ClientNode, tp.ClientReg, tp.Client)
tp.connectClient(t)
time.Sleep(50 * time.Millisecond)
serverID := tp.ServerNode.Identity().ID
stats, err := controller.RemoteStats(serverID)
if err != nil {
t.Fatalf("RemoteStats failed: %v", err)
}
if len(stats.Miners) != 1 {
t.Errorf("expected 1 miner, got %d", len(stats.Miners))
}
}