Repository: FluuxIO/go-xmpp (go)

Issue:
No way to add text context to an XML Node
We have an XMPP component that needs to send an IQ result with an XML element containing JSON. My code to do that looks like this:

```go
jsonData, err := json.Marshal(space)
if err != nil { … }

iqResp := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
iqResp.Any = &stanza.Node{
	XMLName: xml.Name{Space: NSCryphoProtocol, Local: "space"},
	Content: string(jsonData),
}
if err := s.Send(iqResp); err != nil {
	m.errorLog.Printf("Error sending ping response: %v", err)
}
```

This does not work: the generated space element has no content

```xml
<iq type="result" id="id" from="target@domain" to="sender@domain">
  <space xmlns="http://crypho.com/ns/crypho"></space>
</iq>
```

I suspect that the best way to handle this is to add a new `CharData` field to `Node`:

```go
type Node struct {
        XMLName xml.Name
        Attrs   []xml.Attr `xml:"-"`
        Content string     `xml:",innerxml"`
        Nodes   []Node     `xml:",any"`
        CharData string `xml:",chardata"`
}
```

And process that in `MarshalXml`.

Pull request:
Generic node should use cdata instead of innerxml
It fixes #110, but the things to note, is still that the best approach is to create custom payload structs to support custom XMPP stanza. This is already properly described in `stanza` package documentation: https://github.com/FluuxIO/go-xmpp/blob/master/stanza/README.md#adding-your-own-custom-extensions-on-stanzas

See #110 & #111 

Patch:
diff --git a/client.go b/client.go
index 6275920..58d0da1 100644
--- a/client.go
+++ b/client.go
@@ -4,7 +4,6 @@ import (
 	"encoding/xml"
 	"errors"
 	"fmt"
-	"io"
 	"net"
 	"time"
 
@@ -212,7 +211,7 @@ func (c *Client) Send(packet stanza.Packet) error {
 		return errors.New("cannot marshal packet " + err.Error())
 	}
 
-	return c.sendWithWriter(c.Session.streamLogger, data)
+	return c.sendWithLogger(string(data))
 }
 
 // SendRaw sends an XMPP stanza as a string to the server.
@@ -225,12 +224,12 @@ func (c *Client) SendRaw(packet string) error {
 		return errors.New("client is not connected")
 	}
 
-	return c.sendWithWriter(c.Session.streamLogger, []byte(packet))
+	return c.sendWithLogger(packet)
 }
 
-func (c *Client) sendWithWriter(writer io.Writer, packet []byte) error {
+func (c *Client) sendWithLogger(packet string) error {
 	var err error
-	_, err = writer.Write(packet)
+	_, err = fmt.Fprintf(c.Session.streamLogger, packet)
 	return err
 }
 
diff --git a/stanza/iq.go b/stanza/iq.go
index 28405b8..f1663bc 100644
--- a/stanza/iq.go
+++ b/stanza/iq.go
@@ -22,7 +22,7 @@ type IQ struct { // Info/Query
 	//    request."
 	Payload IQPayload `xml:",omitempty"`
 	Error   Err       `xml:"error,omitempty"`
-	// Any is used to decode unknown payload as a generique structure
+	// Any is used to decode unknown payload as a generic structure
 	Any *Node `xml:",any"`
 }
 
diff --git a/stanza/node.go b/stanza/node.go
index 26a3e03..6afa7bc 100644
--- a/stanza/node.go
+++ b/stanza/node.go
@@ -10,7 +10,7 @@ import "encoding/xml"
 type Node struct {
 	XMLName xml.Name
 	Attrs   []xml.Attr `xml:"-"`
-	Content string     `xml:",innerxml"`
+	Content string     `xml:",cdata"`
 	Nodes   []Node     `xml:",any"`
 }
 
@@ -47,5 +47,8 @@ func (n Node) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
 
 	err = e.EncodeToken(start)
 	e.EncodeElement(n.Nodes, xml.StartElement{Name: n.XMLName})
+	if n.Content != "" {
+		e.EncodeToken(xml.CharData(n.Content))
+	}
 	return e.EncodeToken(xml.EndElement{Name: start.Name})
 }


Test patch:
diff --git a/client_internal_test.go b/client_internal_test.go
deleted file mode 100644
index 6daef09..0000000
--- a/client_internal_test.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package xmpp
-
-import (
-	"bytes"
-	"testing"
-)
-
-func TestClient_Send(t *testing.T) {
-	buffer := bytes.NewBufferString("")
-	client := Client{}
-	data := []byte("https://da.wikipedia.org/wiki/J%C3%A6vnd%C3%B8gn")
-	if err := client.sendWithWriter(buffer, data); err != nil {
-		t.Errorf("Writing failed: %v", err)
-	}
-
-	if buffer.String() != string(data) {
-		t.Errorf("Incorrect value sent to buffer: '%s'", buffer.String())
-	}
-}
diff --git a/stanza/error_test.go b/stanza/error_test.go
new file mode 100644
index 0000000..5d933a5
--- /dev/null
+++ b/stanza/error_test.go
@@ -0,0 +1,31 @@
+package stanza
+
+import (
+	"encoding/xml"
+	"testing"
+)
+
+func TestErr_UnmarshalXML(t *testing.T) {
+	packet := `
+ <iq from='pubsub.example.com'
+       id='kj4vz31m'
+       to='romeo@example.net/foo'
+       type='error'>
+  <error type='wait'>
+    <resource-constraint
+        xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
+    <text xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'>System overloaded, please retry</text>
+  </error>
+ </iq>`
+
+	parsedIQ := IQ{}
+	data := []byte(packet)
+	if err := xml.Unmarshal(data, &parsedIQ); err != nil {
+		t.Errorf("Unmarshal(%s) returned error", data)
+	}
+
+	xmppError := parsedIQ.Error
+	if xmppError.Text != "System overloaded, please retry" {
+		t.Errorf("Could not extract error text: '%s'", xmppError.Text)
+	}
+}
diff --git a/stanza/iot_test.go b/stanza/iot_test.go
index 43a60ec..c0953e9 100644
--- a/stanza/iot_test.go
+++ b/stanza/iot_test.go
@@ -21,6 +21,6 @@ func TestControlSet(t *testing.T) {
 	}
 
 	if cs, ok := parsedIQ.Payload.(*ControlSet); !ok {
-		t.Errorf("Paylod is not an iot control set: %v", cs)
+		t.Errorf("Payload is not an iot control set: %v", cs)
 	}
 }
diff --git a/stanza/node_test.go b/stanza/node_test.go
new file mode 100644
index 0000000..aae699d
--- /dev/null
+++ b/stanza/node_test.go
@@ -0,0 +1,30 @@
+package stanza
+
+import (
+	"encoding/xml"
+	"testing"
+)
+
+func TestNode_Marshal(t *testing.T) {
+	jsonData := []byte("{\"key\":\"value\"}")
+
+	iqResp := NewIQ(Attrs{Type: "result", From: "admin@localhost", To: "test@localhost", Id: "1"})
+	iqResp.Any = &Node{
+		XMLName: xml.Name{Space: "myNS", Local: "space"},
+		Content: string(jsonData),
+	}
+
+	bytes, err := xml.Marshal(iqResp)
+	if err != nil {
+		t.Errorf("Could not marshal XML: %v", err)
+	}
+
+	parsedIQ := IQ{}
+	if err := xml.Unmarshal(bytes, &parsedIQ); err != nil {
+		t.Errorf("Unmarshal returned error: %v", err)
+	}
+
+	if parsedIQ.Any.Content != string(jsonData) {
+		t.Errorf("Cannot find generic any payload in parsedIQ: '%s'", parsedIQ.Any.Content)
+	}
+}