~samwhited/xmpp

6518e2f60cc1322c7b90e5f6b6f9af9adf58eb6e — Sam Whited 3 months ago c238636
stanza: add Error method to Presence and Message

Like IQs replying with an error involves switching the to/from
attributes and making sure the type is set to "error" so it makes sense
to have the Error method on both Presence and Message too.

While doing this I noticed that each stanza marshaled its attribute
order differently. While it doesn't matter and this is not a guaranteed
part of the API, it would be nice if they were consistent so I also
switched those around and fixed a few minor documentation issues.

Finally, since we were adding a Presence and Message Error method with a
test I also added a test belatedly for the IQ Error method.

Signed-off-by: Sam Whited <sam@samwhited.com>
M CHANGELOG.md => CHANGELOG.md +1 -0
@@ 21,6 21,7 @@ All notable changes to this project will be documented in this file.
- commands: new package implementing [XEP-0050: Ad-Hoc Commands]
- stanza: implement [XEP-0203: Delayed Delivery]
- stanza: more general `UnmarshalError` function that doesn't focus on IQs
- stanza: add `Error` method to `Presence` and `Message`


### Fixed

M send_test.go => send_test.go +1 -1
@@ 236,7 236,7 @@ var sendTests = [...]struct {
	},
	4: {
		r:   stanza.Presence{To: to, Type: stanza.SubscribePresence, ID: "123"}.Wrap(nil),
		out: `<presence xmlns="jabber:client" id="123" to="test@example.net" type="subscribe"></presence>`,
		out: `<presence xmlns="jabber:client" type="subscribe" to="test@example.net" id="123"></presence>`,
	},
	5: {
		r: stanza.IQ{Type: stanza.ResultIQ}.Wrap(nil),

M stanza/iq.go => stanza/iq.go +2 -3
@@ 127,9 127,8 @@ func (iq IQ) Result(payload xml.TokenReader) xml.TokenReader {
	return iq.Wrap(payload)
}

// Error returns a token reader that wraps the first element from payload in an
// IQ stanza with the to and from attributes switched and the type set to
// ErrorIQ.
// Error returns a token reader that wraps the provided Error in an IQ stanza
// with the to and from attributes switched and the type set to ErrorIQ.
func (iq IQ) Error(err Error) xml.TokenReader {
	iq.Type = ErrorIQ
	iq.From, iq.To = iq.To, iq.From

M stanza/iq_test.go => stanza/iq_test.go +23 -0
@@ 252,3 252,26 @@ func TestUnmarshalIQError(t *testing.T) {
		t.Fatalf("wrong error type: want=%T, got=%T (%[2]v)", stanza.Error{}, err)
	}
}

func TestIQError(t *testing.T) {
	iq := stanza.IQ{
		To:   jid.MustParse("to"),
		From: jid.MustParse("from"),
	}.Error(stanza.Error{
		Condition: stanza.BadRequest,
	})
	var buf bytes.Buffer
	e := xml.NewEncoder(&buf)
	_, err := xmlstream.Copy(e, iq)
	if err != nil {
		t.Fatalf("error encoding stream: %v", err)
	}
	err = e.Flush()
	if err != nil {
		t.Fatalf("error flushing stream: %v", err)
	}
	const expected = `<iq type="error" to="from" from="to"><error><bad-request xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"></bad-request></error></iq>`
	if out := buf.String(); expected != out {
		t.Errorf("unexpected output:\nwant=%s,\n got=%s", expected, out)
	}
}

M stanza/message.go => stanza/message.go +12 -3
@@ 73,15 73,15 @@ func (msg Message) StartElement() xml.StartElement {

	attr := make([]xml.Attr, 0, 5)
	attr = append(attr, xml.Attr{Name: xml.Name{Local: "type"}, Value: string(msg.Type)})
	if msg.ID != "" {
		attr = append(attr, xml.Attr{Name: xml.Name{Local: "id"}, Value: msg.ID})
	}
	if !msg.To.Equal(jid.JID{}) {
		attr = append(attr, xml.Attr{Name: xml.Name{Local: "to"}, Value: msg.To.String()})
	}
	if !msg.From.Equal(jid.JID{}) {
		attr = append(attr, xml.Attr{Name: xml.Name{Local: "from"}, Value: msg.From.String()})
	}
	if msg.ID != "" {
		attr = append(attr, xml.Attr{Name: xml.Name{Local: "id"}, Value: msg.ID})
	}
	if msg.Lang != "" {
		attr = append(attr, xml.Attr{Name: xml.Name{Space: ns.XML, Local: "lang"}, Value: msg.Lang})
	}


@@ 97,6 97,15 @@ func (msg Message) Wrap(payload xml.TokenReader) xml.TokenReader {
	return xmlstream.Wrap(payload, msg.StartElement())
}

// Error returns a token reader that wraps the provided Error in a message
// stanza with the to and from attributes switched and the type set to
// ErrorMessage.
func (msg Message) Error(err Error) xml.TokenReader {
	msg.Type = ErrorMessage
	msg.From, msg.To = msg.To, msg.From
	return msg.Wrap(err.TokenReader())
}

// MessageType is the type of a message stanza.
// It should normally be one of the constants defined in this package.
type MessageType string

M stanza/message_test.go => stanza/message_test.go +24 -0
@@ 10,6 10,7 @@ import (
	"fmt"
	"testing"

	"mellium.im/xmlstream"
	"mellium.im/xmpp/internal/attr"
	"mellium.im/xmpp/internal/ns"
	"mellium.im/xmpp/jid"


@@ 177,3 178,26 @@ func getLangAttr(start xml.StartElement) xml.Attr {
	}
	return langAttr
}

func TestMessageError(t *testing.T) {
	msg := stanza.Message{
		To:   jid.MustParse("to"),
		From: jid.MustParse("from"),
	}.Error(stanza.Error{
		Condition: stanza.BadRequest,
	})
	var buf bytes.Buffer
	e := xml.NewEncoder(&buf)
	_, err := xmlstream.Copy(e, msg)
	if err != nil {
		t.Fatalf("error encoding stream: %v", err)
	}
	err = e.Flush()
	if err != nil {
		t.Fatalf("error flushing stream: %v", err)
	}
	const expected = `<message type="error" to="from" from="to"><error><bad-request xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"></bad-request></error></message>`
	if out := buf.String(); expected != out {
		t.Errorf("unexpected output:\nwant=%s,\n got=%s", expected, out)
	}
}

M stanza/presence.go => stanza/presence.go +14 -5
@@ 72,8 72,8 @@ func (p Presence) StartElement() xml.StartElement {
	name.Local = "presence"

	attr := make([]xml.Attr, 0, 5)
	if p.ID != "" {
		attr = append(attr, xml.Attr{Name: xml.Name{Local: "id"}, Value: p.ID})
	if p.Type != "" {
		attr = append(attr, xml.Attr{Name: xml.Name{Local: "type"}, Value: string(p.Type)})
	}
	if !p.To.Equal(jid.JID{}) {
		attr = append(attr, xml.Attr{Name: xml.Name{Local: "to"}, Value: p.To.String()})


@@ 81,12 81,12 @@ func (p Presence) StartElement() xml.StartElement {
	if !p.From.Equal(jid.JID{}) {
		attr = append(attr, xml.Attr{Name: xml.Name{Local: "from"}, Value: p.From.String()})
	}
	if p.ID != "" {
		attr = append(attr, xml.Attr{Name: xml.Name{Local: "id"}, Value: p.ID})
	}
	if p.Lang != "" {
		attr = append(attr, xml.Attr{Name: xml.Name{Space: ns.XML, Local: "lang"}, Value: p.Lang})
	}
	if p.Type != "" {
		attr = append(attr, xml.Attr{Name: xml.Name{Local: "type"}, Value: string(p.Type)})
	}

	return xml.StartElement{
		Name: name,


@@ 102,6 102,15 @@ func (p Presence) Wrap(payload xml.TokenReader) xml.TokenReader {
	return xmlstream.Wrap(payload, p.StartElement())
}

// Error returns a token reader that wraps the provided Error in a presence
// stanza with the to and from attributes switched and the type set to
// ErrorPresence.
func (p Presence) Error(err Error) xml.TokenReader {
	p.Type = ErrorPresence
	p.From, p.To = p.To, p.From
	return p.Wrap(err.TokenReader())
}

// PresenceType is the type of a presence stanza.
// It should normally be one of the constants defined in this package.
type PresenceType string

M stanza/presence_test.go => stanza/presence_test.go +24 -1
@@ 38,7 38,7 @@ var wrapPresenceTests = [...]struct {
	3: {
		to:  exampleJID,
		typ: stanza.SubscribedPresence,
		out: `<presence to="example.net" type="subscribed"></presence>`,
		out: `<presence type="subscribed" to="example.net"></presence>`,
	},
	4: {
		payload: &testReader{},


@@ 212,3 212,26 @@ func TestPresenceFromStartElement(t *testing.T) {
		})
	}
}

func TestPresenceError(t *testing.T) {
	pres := stanza.Presence{
		To:   jid.MustParse("to"),
		From: jid.MustParse("from"),
	}.Error(stanza.Error{
		Condition: stanza.BadRequest,
	})
	var buf bytes.Buffer
	e := xml.NewEncoder(&buf)
	_, err := xmlstream.Copy(e, pres)
	if err != nil {
		t.Fatalf("error encoding stream: %v", err)
	}
	err = e.Flush()
	if err != nil {
		t.Fatalf("error flushing stream: %v", err)
	}
	const expected = `<presence type="error" to="from" from="to"><error><bad-request xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"></bad-request></error></presence>`
	if out := buf.String(); expected != out {
		t.Errorf("unexpected output:\nwant=%s,\n got=%s", expected, out)
	}
}