~samwhited/xmpp

d138ede52e6d7a941cb5fa6c135d9792f1a2a832 — Sam Whited 5 years ago c07b516
Add marshaling of stanza errors (and tests)
2 files changed, 147 insertions(+), 0 deletions(-)

M errors.go
M errors_test.go
M errors.go => errors.go +87 -0
@@ 152,3 152,90 @@ func (se StanzaError) MarshalXML(e *xml.Encoder, start xml.StartElement) (err er
	e.EncodeToken(start.End())
	return nil
}

func (se *StanzaError) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
	decoded := struct {
		Condition struct {
			XMLName xml.Name
		} `xml:",any"`
		Type errorType `xml:"type,attr"`
		By   *jid.JID  `xml:"by,attr"`
		Text []struct {
			Lang string `xml:"http://www.w3.org/XML/1998/namespace lang,attr"`
			Data string `xml:",chardata"`
		} `xml:"urn:ietf:params:xml:ns:xmpp-stanzas text"`
	}{}
	if err := d.DecodeElement(&decoded, &start); err != nil {
		return err
	}
	se.Type = decoded.Type
	se.By = decoded.By
	// TODO: Oh god why… maybe I should transform the String() output instead.
	switch decoded.Condition.XMLName.Local {
	case "bad-request":
		se.Condition = BadRequest
	case "conflict":
		se.Condition = Conflict
	case "feature-not-implemented":
		se.Condition = FeatureNotImplemented
	case "forbidden":
		se.Condition = Forbidden
	case "gone":
		se.Condition = Gone
	case "internal-server-error":
		se.Condition = InternalServerError
	case "item-not-found":
		se.Condition = ItemNotFound
	case "jid-malformed":
		se.Condition = JIDMalformed
	case "not-acceptable":
		se.Condition = NotAcceptable
	case "not-allowed":
		se.Condition = NotAllowed
	case "not-authorized":
		se.Condition = NotAuthorized
	case "policy-violation":
		se.Condition = PolicyViolation
	case "recipient-unavailable":
		se.Condition = RecipientUnavailable
	case "redirect":
		se.Condition = Redirect
	case "registration-required":
		se.Condition = RegistrationRequired
	case "remote-server-not-found":
		se.Condition = RemoteServerNotFound
	case "remote-server-timeout":
		se.Condition = RemoteServerTimeout
	case "resource-constraint":
		se.Condition = ResourceConstraint
	case "service-unavailable":
		se.Condition = ServiceUnavailable
	case "subscription-required":
		se.Condition = SubscriptionRequired
	case "undefined-condition":
		se.Condition = UndefinedCondition
	case "unexpected-request":
		se.Condition = UnexpectedRequest
	default:
		if decoded.Condition.XMLName.Space == ns.Stanza {
			se.Condition = condition(decoded.Condition.XMLName.Local)
		}
	}

	// TODO: Dedup this (and probably a lot of other stuff) with the saslerr
	// logic.
	tags := make([]language.Tag, 0, len(decoded.Text))
	data := make(map[language.Tag]string)
	for _, text := range decoded.Text {
		tag, err := language.Parse(text.Lang)
		if err != nil {
			continue
		}
		tags = append(tags, tag)
		data[tag] = text.Data
	}
	tag, _, _ := language.NewMatcher(tags).Match(se.Lang)
	se.Lang = tag
	se.Text, _ = data[tag]
	return nil
}

M errors_test.go => errors_test.go +60 -0
@@ 9,6 9,7 @@ import (
	"fmt"
	"testing"

	"golang.org/x/text/language"
	"mellium.im/xmpp/jid"
)



@@ 58,3 59,62 @@ func TestMarshalStanzaError(t *testing.T) {
		}
	}
}

func TestUnmarshalStanzaError(t *testing.T) {
	for _, data := range []struct {
		xml  string
		lang language.Tag
		se   StanzaError
		err  bool
	}{
		{"", language.Und, StanzaError{}, true},
		{`<error><unexpected-request xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"></unexpected-request></error>`,
			language.Und, StanzaError{Condition: UnexpectedRequest}, false},
		{`<error type="cancel"><registration-required xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"></registration-required></error>`,
			language.Und, StanzaError{Condition: RegistrationRequired}, false},
		{`<error type="cancel"><redirect xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"></redirect></error>`,
			language.Und, StanzaError{Type: Cancel, Condition: Redirect}, false},
		{`<error type="wait"><undefined-condition xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"></undefined-condition></error>`,
			language.Und, StanzaError{Type: Wait, Condition: UndefinedCondition}, false},
		{`<error type="modify" by="test@example.net"><subscription-required xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"></subscription-required></error>`,
			language.Und, StanzaError{Type: Modify, By: jid.MustParse("test@example.net"), Condition: SubscriptionRequired}, false},
		{`<error type="continue"><service-unavailable xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"></service-unavailable><text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" xml:lang="und">test</text></error>`,
			language.Und, StanzaError{Type: Continue, Condition: ServiceUnavailable, Text: "test"}, false},
		{`<error type="auth"><resource-constraint xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"></resource-constraint><text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" xml:lang="en">test</text></error>`,
			language.Und, StanzaError{Type: Auth, Condition: ResourceConstraint, Text: "test", Lang: language.English}, false},
		{`<error type="auth"><resource-constraint xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"></resource-constraint><text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" xml:lang="en">test</text><text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" xml:lang="de">German</text></error>`,
			language.German, StanzaError{Type: Auth, Condition: ResourceConstraint, Text: "German", Lang: language.German}, false},
		{`<error type="auth"><remote-server-timeout xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"></remote-server-timeout><text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" xml:lang="en">test</text><text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" xml:lang="es">Spanish</text></error>`,
			language.LatinAmericanSpanish, StanzaError{Type: Auth, Condition: RemoteServerTimeout, Text: "Spanish", Lang: language.Spanish}, false},
		{`<error by=""><remote-server-not-found xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"></remote-server-not-found></error>`,
			language.Und, StanzaError{By: &jid.JID{}, Condition: RemoteServerNotFound}, false},
		{`<error><other xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"></other></error>`,
			language.Und, StanzaError{Condition: condition("other")}, false},
		{`<error><recipient-unavailable xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"></recipient-unavailable><text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" xml:lang="ac-u">test</text></error>`,
			language.Und, StanzaError{Condition: RecipientUnavailable}, false},
	} {
		se2 := StanzaError{Lang: data.lang}
		err := xml.Unmarshal([]byte(data.xml), &se2)
		j1, j2 := data.se.By, se2.By
		data.se.By = nil
		se2.By = nil
		switch {
		case data.err && err == nil:
			t.Errorf("Expected an error when unmarshaling stanza error `%s`", data.xml)
			continue
		case !data.err && err != nil:
			t.Error(err)
			continue
		case err != nil:
			continue
		case !j1.Equal(j2):
			t.Errorf(`Expected by="%v" but got by="%v"`, j1, j2)
		case data.se.Lang != se2.Lang:
			// This case is included in the next one, but I wanted it to print
			// something nicer for languages…
			t.Errorf("Expected unmarshaled stanza error to have lang `%s` but got `%s`.", data.se.Lang, se2.Lang)
		case data.se != se2:
			t.Errorf("Expected unmarshaled stanza error `%#v` but got `%#v`.", data.se, se2)
		}
	}
}