From 145fce6b3f6a465fa66cb51975d275131226559b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20R=C3=A9mond?= Date: Fri, 21 Jun 2019 16:48:13 +0200 Subject: [PATCH] Add StanzaType matcher / Clarify empty route behaviour (#65) * Add route to match on stanza type * Add test checking that an empty route "always" matches --- router.go | 39 +++++++++++++- router_test.go | 143 +++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 176 insertions(+), 6 deletions(-) diff --git a/router.go b/router.go index 5b4515b..5264a95 100644 --- a/router.go +++ b/router.go @@ -12,6 +12,9 @@ Here are important rules to keep in mind while setting your routes and matchers: - Routes are evaluated in the order they are set. - When a route matches, it is executed and all others routes are ignored. For each packet, only a single route is executed. +- An empty route will match everything. Adding an empty route as the last route in your router will + allow you to get all stanzas that did not match any previous route. You can for example use this to + log all unexpected stanza received by your client or component. TODO: Automatically reply to IQ that do not match any route, to comply to XMPP standard. */ @@ -145,6 +148,41 @@ func (r *Route) Packet(name string) *Route { return r.addMatcher(nameMatcher(name)) } +// ------------------------- +// Match on stanza type + +// nsTypeMather matches on a list of IQ payload namespaces +type nsTypeMatcher []string + +func (m nsTypeMatcher) Match(p Packet, match *RouteMatch) bool { + // TODO: Rework after merge of #62 + var stanzaType string + switch packet := p.(type) { + case IQ: + stanzaType = packet.Type + case Presence: + stanzaType = packet.Type + case Message: + if packet.Type == "" { + // optional on message, normal is the default type + stanzaType = "normal" + } else { + stanzaType = packet.Type + } + default: + return false + } + return matchInArray(m, stanzaType) +} + +// IQNamespaces adds an IQ matcher, expecting both an IQ and a +func (r *Route) StanzaType(types ...string) *Route { + for k, v := range types { + types[k] = strings.ToLower(v) + } + return r.addMatcher(nsTypeMatcher(types)) +} + // ------------------------- // Match on IQ and namespace @@ -152,7 +190,6 @@ func (r *Route) Packet(name string) *Route { type nsIQMatcher []string func (m nsIQMatcher) Match(p Packet, match *RouteMatch) bool { - // TODO iq, ok := p.(IQ) if !ok { return false diff --git a/router_test.go b/router_test.go index 4e44f41..4b6efe6 100644 --- a/router_test.go +++ b/router_test.go @@ -8,9 +8,6 @@ import ( "gosrc.io/xmpp" ) -// ============================================================================ -// SenderMock - // ============================================================================ // Test route & matchers @@ -51,7 +48,6 @@ func TestIQNSMatcher(t *testing.T) { // Check that an IQ with proper namespace does match conn := NewSenderMock() iqDisco := xmpp.NewIQ("get", "", "localhost", "1", "") - // TODO: Add a function to generate payload with proper namespace initialisation iqDisco.Payload = &xmpp.DiscoInfo{ XMLName: xml.Name{ Space: xmpp.NSDiscoInfo, @@ -65,7 +61,6 @@ func TestIQNSMatcher(t *testing.T) { // Check that another namespace is not matched conn = NewSenderMock() iqVersion := xmpp.NewIQ("get", "", "localhost", "1", "") - // TODO: Add a function to generate payload with proper namespace initialisation iqVersion.Payload = &xmpp.DiscoInfo{ XMLName: xml.Name{ Space: "jabber:iq:version", @@ -77,6 +72,144 @@ func TestIQNSMatcher(t *testing.T) { } } +func TestTypeMatcher(t *testing.T) { + router := xmpp.NewRouter() + router.NewRoute(). + StanzaType("normal"). + HandlerFunc(func(s xmpp.Sender, p xmpp.Packet) { + _ = s.SendRaw(successFlag) + }) + + // Check that a packet with the proper type matches + conn := NewSenderMock() + message := xmpp.NewMessage("normal", "", "test@localhost", "1", "") + message.Body = "hello" + router.Route(conn, message) + + if conn.String() != successFlag { + t.Errorf("'normal' message should have been matched and routed: %v", message) + } + + // We should match on default type 'normal' for message without a type + conn = NewSenderMock() + message = xmpp.NewMessage("", "", "test@localhost", "1", "") + message.Body = "hello" + router.Route(conn, message) + + if conn.String() != successFlag { + t.Errorf("message should have been matched and routed: %v", message) + } + + // We do not match on other types + conn = NewSenderMock() + iqVersion := xmpp.NewIQ("get", "service.localhost", "test@localhost", "1", "") + iqVersion.Payload = &xmpp.DiscoInfo{ + XMLName: xml.Name{ + Space: "jabber:iq:version", + Local: "query", + }} + router.Route(conn, iqVersion) + + if conn.String() == successFlag { + t.Errorf("iq get should not have been matched and routed: %v", iqVersion) + } +} + +func TestCompositeMatcher(t *testing.T) { + router := xmpp.NewRouter() + router.NewRoute(). + IQNamespaces("jabber:iq:version"). + StanzaType("get"). + HandlerFunc(func(s xmpp.Sender, p xmpp.Packet) { + _ = s.SendRaw(successFlag) + }) + + // Data set + getVersionIq := xmpp.NewIQ("get", "service.localhost", "test@localhost", "1", "") + getVersionIq.Payload = &xmpp.Version{ + XMLName: xml.Name{ + Space: "jabber:iq:version", + Local: "query", + }} + + setVersionIq := xmpp.NewIQ("set", "service.localhost", "test@localhost", "1", "") + setVersionIq.Payload = &xmpp.Version{ + XMLName: xml.Name{ + Space: "jabber:iq:version", + Local: "query", + }} + + GetDiscoIq := xmpp.NewIQ("get", "service.localhost", "test@localhost", "1", "") + GetDiscoIq.Payload = &xmpp.DiscoInfo{ + XMLName: xml.Name{ + Space: "http://jabber.org/protocol/disco#info", + Local: "query", + }} + + message := xmpp.NewMessage("normal", "", "test@localhost", "1", "") + message.Body = "hello" + + tests := []struct { + name string + input xmpp.Packet + want bool + }{ + {name: "match get version iq", input: getVersionIq, want: true}, + {name: "ignore set version iq", input: setVersionIq, want: false}, + {name: "ignore get discoinfo iq", input: GetDiscoIq, want: false}, + {name: "ignore message", input: message, want: false}, + } + + // + for _, tc := range tests { + t.Run(tc.name, func(st *testing.T) { + conn := NewSenderMock() + router.Route(conn, tc.input) + + res := conn.String() == successFlag + if tc.want != res { + st.Errorf("incorrect result for %#v\nMatch = %#v, expecting %#v", tc.input, res, tc.want) + } + }) + } +} + +// A blank route with empty matcher will always match +// It can be use to receive all packets that do not match any of the previous route. +func TestCatchallMatcher(t *testing.T) { + router := xmpp.NewRouter() + router.NewRoute(). + HandlerFunc(func(s xmpp.Sender, p xmpp.Packet) { + _ = s.SendRaw(successFlag) + }) + + // Check that we match on several packets + conn := NewSenderMock() + message := xmpp.NewMessage("chat", "", "test@localhost", "1", "") + message.Body = "hello" + router.Route(conn, message) + + if conn.String() != successFlag { + t.Errorf("chat message should have been matched and routed: %v", message) + } + + conn = NewSenderMock() + iqVersion := xmpp.NewIQ("get", "service.localhost", "test@localhost", "1", "") + iqVersion.Payload = &xmpp.DiscoInfo{ + XMLName: xml.Name{ + Space: "jabber:iq:version", + Local: "query", + }} + router.Route(conn, iqVersion) + + if conn.String() != successFlag { + t.Errorf("iq get should have been matched and routed: %v", iqVersion) + } +} + +// ============================================================================ +// SenderMock + var successFlag = "matched" type SenderMock struct {