package node import ( "testing" "time" ) func TestMessage_NewMessage_Good(t *testing.T) { t.Run("BasicMessage", func(t *testing.T) { msg, err := NewMessage(MessagePing, "sender-id", "receiver-id", nil) if err != nil { t.Fatalf("failed to create message: %v", err) } if msg.Type != MessagePing { t.Errorf("expected type MessagePing, got %s", msg.Type) } if msg.From != "sender-id" { t.Errorf("expected from 'sender-id', got '%s'", msg.From) } if msg.To != "receiver-id" { t.Errorf("expected to 'receiver-id', got '%s'", msg.To) } if msg.ID == "" { t.Error("message ID should not be empty") } if msg.Timestamp.IsZero() { t.Error("timestamp should be set") } }) t.Run("MessageWithPayload", func(t *testing.T) { payload := PingPayload{ SentAt: time.Now().UnixMilli(), } msg, err := NewMessage(MessagePing, "sender", "receiver", payload) if err != nil { t.Fatalf("failed to create message: %v", err) } if msg.Payload == nil { t.Error("payload should not be nil") } var parsed PingPayload err = msg.ParsePayload(&parsed) if err != nil { t.Fatalf("failed to parse payload: %v", err) } if parsed.SentAt != payload.SentAt { t.Errorf("expected SentAt %d, got %d", payload.SentAt, parsed.SentAt) } }) } func TestMessage_Reply_Good(t *testing.T) { original, _ := NewMessage(MessagePing, "sender", "receiver", PingPayload{SentAt: 12345}) reply, err := original.Reply(MessagePong, PongPayload{ SentAt: 12345, ReceivedAt: 12350, }) if err != nil { t.Fatalf("failed to create reply: %v", err) } if reply.ReplyTo != original.ID { t.Errorf("reply should reference original message ID") } if reply.From != original.To { t.Error("reply From should be original To") } if reply.To != original.From { t.Error("reply To should be original From") } if reply.Type != MessagePong { t.Errorf("expected type MessagePong, got %s", reply.Type) } } func TestMessage_ParsePayload_Good(t *testing.T) { t.Run("ValidPayload", func(t *testing.T) { payload := StartMinerPayload{ MinerType: "xmrig", ProfileID: "test-profile", } msg, _ := NewMessage(MessageStartMiner, "ctrl", "worker", payload) var parsed StartMinerPayload err := msg.ParsePayload(&parsed) if err != nil { t.Fatalf("failed to parse payload: %v", err) } if parsed.ProfileID != "test-profile" { t.Errorf("expected ProfileID 'test-profile', got '%s'", parsed.ProfileID) } }) t.Run("NilPayload", func(t *testing.T) { msg, _ := NewMessage(MessageGetStats, "ctrl", "worker", nil) var parsed StatsPayload err := msg.ParsePayload(&parsed) if err != nil { t.Errorf("parsing nil payload should not error: %v", err) } }) t.Run("ComplexPayload", func(t *testing.T) { stats := StatsPayload{ NodeID: "node-123", NodeName: "Test Node", Miners: []MinerStatsItem{ { Name: "xmrig-1", Type: "xmrig", Hashrate: 1234.56, Shares: 100, Rejected: 2, Uptime: 3600, Pool: "pool.example.com:3333", Algorithm: "RandomX", }, }, Uptime: 86400, } msg, _ := NewMessage(MessageStats, "worker", "ctrl", stats) var parsed StatsPayload err := msg.ParsePayload(&parsed) if err != nil { t.Fatalf("failed to parse stats payload: %v", err) } if parsed.NodeID != "node-123" { t.Errorf("expected NodeID 'node-123', got '%s'", parsed.NodeID) } if len(parsed.Miners) != 1 { t.Fatalf("expected 1 miner, got %d", len(parsed.Miners)) } if parsed.Miners[0].Hashrate != 1234.56 { t.Errorf("expected hashrate 1234.56, got %f", parsed.Miners[0].Hashrate) } }) } func TestMessage_NewErrorMessage_Bad(t *testing.T) { errMsg, err := NewErrorMessage("sender", "receiver", ErrorCodeOperationFailed, "something went wrong", "original-msg-id") if err != nil { t.Fatalf("failed to create error message: %v", err) } if errMsg.Type != MessageError { t.Errorf("expected type MessageError, got %s", errMsg.Type) } if errMsg.ReplyTo != "original-msg-id" { t.Errorf("expected ReplyTo 'original-msg-id', got '%s'", errMsg.ReplyTo) } var errPayload ErrorPayload err = errMsg.ParsePayload(&errPayload) if err != nil { t.Fatalf("failed to parse error payload: %v", err) } if errPayload.Code != ErrorCodeOperationFailed { t.Errorf("expected code %d, got %d", ErrorCodeOperationFailed, errPayload.Code) } if errPayload.Message != "something went wrong" { t.Errorf("expected message 'something went wrong', got '%s'", errPayload.Message) } } func TestMessage_Serialization_Good(t *testing.T) { original, _ := NewMessage(MessageStartMiner, "ctrl", "worker", StartMinerPayload{ MinerType: "xmrig", ProfileID: "my-profile", }) // Serialize data := testJSONMarshal(t, original) // Deserialize var restored Message testJSONUnmarshal(t, data, &restored) if restored.ID != original.ID { t.Error("ID mismatch after serialization") } if restored.Type != original.Type { t.Error("Type mismatch after serialization") } if restored.From != original.From { t.Error("From mismatch after serialization") } var payload StartMinerPayload if err := restored.ParsePayload(&payload); err != nil { t.Fatalf("failed to parse restored payload: %v", err) } if payload.ProfileID != "my-profile" { t.Errorf("expected ProfileID 'my-profile', got '%s'", payload.ProfileID) } } func TestMessage_Types_Good(t *testing.T) { types := []MessageType{ MessageHandshake, MessageHandshakeAck, MessagePing, MessagePong, MessageDisconnect, MessageGetStats, MessageStats, MessageStartMiner, MessageStopMiner, MessageMinerAck, MessageDeploy, MessageDeployAck, MessageGetLogs, MessageLogs, MessageError, } for _, msgType := range types { t.Run(string(msgType), func(t *testing.T) { msg, err := NewMessage(msgType, "from", "to", nil) if err != nil { t.Fatalf("failed to create message of type %s: %v", msgType, err) } if msg.Type != msgType { t.Errorf("expected type %s, got %s", msgType, msg.Type) } }) } } func TestMessage_ErrorCodes_Bad(t *testing.T) { codes := map[int]string{ ErrorCodeUnknown: "Unknown", ErrorCodeInvalidMessage: "InvalidMessage", ErrorCodeUnauthorized: "Unauthorized", ErrorCodeNotFound: "NotFound", ErrorCodeOperationFailed: "OperationFailed", ErrorCodeTimeout: "Timeout", } for code, name := range codes { t.Run(name, func(t *testing.T) { if code < 1000 || code > 1999 { t.Errorf("error code %d should be in 1000-1999 range", code) } }) } } func TestMessage_NewMessage_NilPayload_Ugly(t *testing.T) { msg, err := NewMessage(MessagePing, "from", "to", nil) if err != nil { t.Fatalf("NewMessage with nil payload should succeed: %v", err) } if msg.Payload != nil { t.Error("payload should be nil for nil input") } } func TestMessage_ParsePayload_Nil_Ugly(t *testing.T) { msg := &Message{Payload: nil} var target PingPayload err := msg.ParsePayload(&target) if err != nil { t.Errorf("ParsePayload with nil payload should succeed: %v", err) } } func TestMessage_NewErrorMessage_Success_Bad(t *testing.T) { msg, err := NewErrorMessage("from", "to", ErrorCodeOperationFailed, "something went wrong", "reply-123") if err != nil { t.Fatalf("NewErrorMessage failed: %v", err) } if msg.Type != MessageError { t.Errorf("expected type %s, got %s", MessageError, msg.Type) } if msg.ReplyTo != "reply-123" { t.Errorf("expected ReplyTo 'reply-123', got '%s'", msg.ReplyTo) } var payload ErrorPayload err = msg.ParsePayload(&payload) if err != nil { t.Fatalf("ParsePayload failed: %v", err) } if payload.Code != ErrorCodeOperationFailed { t.Errorf("expected code %d, got %d", ErrorCodeOperationFailed, payload.Code) } if payload.Message != "something went wrong" { t.Errorf("expected message 'something went wrong', got '%s'", payload.Message) } }