From 2ff03d33cd875979a445ba7e45e0517705289d0e Mon Sep 17 00:00:00 2001
From: jandelgado <jandelgado@users.noreply.github.com>
Date: Thu, 5 Nov 2020 21:14:04 +0100
Subject: [PATCH] workaround for RabbitMQ API (#48)

returning "undefined" string in JSON where integer was expected in the consumers API (#47)
---
 CHANGELOG.md                     |  5 +++
 Makefile                         |  2 +-
 pkg/rabbitmq_rest_client.go      | 18 +++++++-
 pkg/rabbitmq_rest_client_test.go | 71 ++++++++++++++++++++++++++++++++
 4 files changed, 93 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ad9f301..fc71ba4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,11 @@
 
 # Changelog for rabtap
 
+## v1.25 (2020-10-30)
+
+* fix: rabtap info: workaround for RabbitMQ API returning an `"undefined"`
+  string where an integer was expected (#47)
+
 ## v1.24 (2020-09-28)
 
 * new: support TLS client certificates (contributed by Francois Gouteroux)
diff --git a/Makefile b/Makefile
index 9afe144..521cb94 100644
--- a/Makefile
+++ b/Makefile
@@ -58,7 +58,7 @@ toxiproxy-cmd:
 
 # run rabbitmq server for integration test using docker container.
 run-broker:
-	podman run -ti --rm -p 5672:5672 -p 15672:15672 rabbitmq:3-management
+	podman run -ti --rm -p 5672:5672 -p 15672:15672 rabbitmq:3.8.5-management
 
 dist-clean: clean
 	rm -f *.out $(BINARY_WIN64) $(BINARY_LINUX64) $(BINARY_DARWIN64)
diff --git a/pkg/rabbitmq_rest_client.go b/pkg/rabbitmq_rest_client.go
index ce63bc4..fefe3b1 100644
--- a/pkg/rabbitmq_rest_client.go
+++ b/pkg/rabbitmq_rest_client.go
@@ -511,10 +511,12 @@ type RabbitExchange struct {
 	} `json:"message_stats,omitempty"`
 }
 
+type OptInt int
+
 // ChannelDetails model channel_details in RabbitConsumer
 type ChannelDetails struct {
 	PeerHost       string `json:"peer_host"`
-	PeerPort       int    `json:"peer_port"`
+	PeerPort       OptInt `json:"peer_port"`
 	ConnectionName string `json:"connection_name"`
 	User           string `json:"user"`
 	Number         int    `json:"number"`
@@ -522,12 +524,24 @@ type ChannelDetails struct {
 	Name           string `json:"name"`
 }
 
+// UnmarshalJSON is a workaround to deserialize int attributes in the
+// RabbitMQ API which are sometimes returned as strings, (i.e. the
+// value "undefined").
+func (d *OptInt) UnmarshalJSON(data []byte) error {
+	if data[0] == '"' {
+		return nil
+	}
+	type Alias int
+	aux := (*Alias)(d)
+	return json.Unmarshal(data, aux)
+}
+
 // UnmarshalJSON is a custom unmarshaler as a WORKAROUND for RabbitMQ API
 // returning "[]" instead of null.  To make sure deserialization does not
 // break, we catch this case, and return an empty ChannelDetails struct.
 // see e.g. https://github.com/rabbitmq/rabbitmq-management/issues/424
 func (d *ChannelDetails) UnmarshalJSON(data []byte) error {
-	// akias ChannelDetails to avoid recursion when callung Unmarshal
+	// alias ChannelDetails to avoid recursion when calling Unmarshal
 	type Alias ChannelDetails
 	aux := &struct {
 		*Alias
diff --git a/pkg/rabbitmq_rest_client_test.go b/pkg/rabbitmq_rest_client_test.go
index af5cec0..d4f49ae 100644
--- a/pkg/rabbitmq_rest_client_test.go
+++ b/pkg/rabbitmq_rest_client_test.go
@@ -4,6 +4,7 @@ package rabtap
 
 import (
 	"crypto/tls"
+	"encoding/json"
 	"fmt"
 	"net/http"
 	"net/http/httptest"
@@ -172,6 +173,76 @@ func TestRabbitClientGetConnections(t *testing.T) {
 	assert.Equal(t, "172.17.0.1:40874 -> 172.17.0.2:5672", conn[0].Name)
 }
 
+func TestRabbitClientDeserializePeerPortInConsumerToInt(t *testing.T) {
+	msg := `
+[
+  {
+    "arguments": {},
+    "ack_required": true,
+    "active": true,
+    "activity_status": "up",
+    "channel_details": {
+      "connection_name": "XXX",
+      "name": "XXX (1)",
+      "node": "XXX",
+      "number": 1,
+      "peer_host": "undefined",
+      "peer_port": 1234,
+      "user": "none"
+    },
+    "consumer_tag": "amq.ctag-InRAvLn4GW3j2mRwPmWJxA",
+    "exclusive": false,
+    "prefetch_count": 20,
+    "queue": {
+      "name": "logstream",
+      "vhost": "/"
+    }
+  }
+]
+`
+	var consumer []RabbitConsumer
+	err := json.Unmarshal([]byte(msg), &consumer)
+	assert.NoError(t, err)
+	assert.Equal(t, OptInt(1234), consumer[0].ChannelDetails.PeerPort)
+
+}
+
+func TestRabbitClientDeserializePeerPortInConsumerAsStringWithoutError(t *testing.T) {
+	// RabbitMQ sometimes returns "undefined" for the peer_port attribute,
+	// but we expect an integer.
+	msg := `
+[
+  {
+    "arguments": {},
+    "ack_required": true,
+    "active": true,
+    "activity_status": "up",
+    "channel_details": {
+      "connection_name": "XXX",
+      "name": "XXX (1)",
+      "node": "XXX",
+      "number": 1,
+      "peer_host": "undefined",
+      "peer_port": "undefined",
+      "user": "none"
+    },
+    "consumer_tag": "amq.ctag-InRAvLn4GW3j2mRwPmWJxA",
+    "exclusive": false,
+    "prefetch_count": 20,
+    "queue": {
+      "name": "logstream",
+      "vhost": "/"
+    }
+  }
+]
+`
+	var consumer []RabbitConsumer
+	err := json.Unmarshal([]byte(msg), &consumer)
+	assert.NoError(t, err)
+	assert.Equal(t, OptInt(0), consumer[0].ChannelDetails.PeerPort)
+
+}
+
 // test of GET /api/consumers endpoint workaround for empty channel_details
 func TestRabbitClientGetConsumersChannelDetailsIsEmptyArray(t *testing.T) {