Introduce Sender interface to abstract client sending in router handlers

This commit is contained in:
Mickael Remond 2019-06-14 09:37:38 +02:00 committed by Mickaël Rémond
parent b05e68c844
commit 9db33d5792
4 changed files with 80 additions and 30 deletions

View file

@ -141,11 +141,11 @@ func (c *Component) recv() (err error) {
// Handle stream errors // Handle stream errors
switch p := val.(type) { switch p := val.(type) {
case StreamError: case StreamError:
c.router.Route(c.conn, val) c.router.Route(c, val)
c.streamError(p.Error.Local, p.Text) c.streamError(p.Error.Local, p.Text)
return errors.New("stream error: " + p.Error.Local) return errors.New("stream error: " + p.Error.Local)
} }
c.router.Route(c.conn, val) c.router.Route(c, val)
} }
} }

View file

@ -1,12 +1,11 @@
package xmpp package xmpp
import ( import (
"io"
"strings" "strings"
) )
/* /*
The XMPP router helps client and component developer select which XMPP they would like to process, The XMPP router helps client and component developers select which XMPP they would like to process,
and associate processing code depending on the router configuration. and associate processing code depending on the router configuration.
TODO: Automatically reply to IQ that do not match any route, to comply to XMPP standard. TODO: Automatically reply to IQ that do not match any route, to comply to XMPP standard.
@ -22,10 +21,10 @@ func NewRouter() *Router {
return &Router{} return &Router{}
} }
func (r *Router) Route(w io.Writer, p Packet) { func (r *Router) Route(s Sender, p Packet) {
var match RouteMatch var match RouteMatch
if r.Match(p, &match) { if r.Match(p, &match) {
match.Handler.HandlePacket(w, p) match.Handler.HandlePacket(s, p)
} }
} }
@ -53,14 +52,14 @@ func (r *Router) Handle(name string, handler Handler) *Route {
// HandleFunc registers a new route with a matcher for for a given packet name (iq, message, presence) // HandleFunc registers a new route with a matcher for for a given packet name (iq, message, presence)
// See Route.Path() and Route.HandlerFunc(). // See Route.Path() and Route.HandlerFunc().
func (r *Router) HandleFunc(name string, f func(io.Writer, Packet)) *Route { func (r *Router) HandleFunc(name string, f func(s Sender, p Packet)) *Route {
return r.NewRoute().Packet(name).HandlerFunc(f) return r.NewRoute().Packet(name).HandlerFunc(f)
} }
// ============================================================================ // ============================================================================
// Route // Route
type Handler interface { type Handler interface {
HandlePacket(w io.Writer, p Packet) HandlePacket(s Sender, p Packet)
} }
type Route struct { type Route struct {
@ -78,11 +77,11 @@ func (r *Route) Handler(handler Handler) *Route {
// ordinary functions as XMPP handlers. If f is a function // ordinary functions as XMPP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a // with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f. // Handler that calls f.
type HandlerFunc func(io.Writer, Packet) type HandlerFunc func(s Sender, p Packet)
// HandlePacket calls f(w, p) // HandlePacket calls f(s, p)
func (f HandlerFunc) HandlePacket(w io.Writer, p Packet) { func (f HandlerFunc) HandlePacket(s Sender, p Packet) {
f(w, p) f(s, p)
} }
// HandlerFunc sets a handler function for the route // HandlerFunc sets a handler function for the route

View file

@ -3,36 +3,39 @@ package xmpp_test
import ( import (
"bytes" "bytes"
"encoding/xml" "encoding/xml"
"io"
"testing" "testing"
"gosrc.io/xmpp" "gosrc.io/xmpp"
) )
var successFlag = []byte("matched") // ============================================================================
// SenderMock
// ============================================================================
// Test route & matchers
func TestNameMatcher(t *testing.T) { func TestNameMatcher(t *testing.T) {
router := xmpp.NewRouter() router := xmpp.NewRouter()
router.HandleFunc("message", func(w io.Writer, p xmpp.Packet) { router.HandleFunc("message", func(s xmpp.Sender, p xmpp.Packet) {
_, _ = w.Write(successFlag) _ = s.SendRaw(successFlag)
}) })
// Check that a message packet is properly matched // Check that a message packet is properly matched
var buf bytes.Buffer conn := NewSenderMock()
// TODO: We want packet creation code to use struct to use default values // TODO: We want packet creation code to use struct to use default values
msg := xmpp.NewMessage("chat", "", "test@localhost", "1", "") msg := xmpp.NewMessage("chat", "", "test@localhost", "1", "")
msg.Body = "Hello" msg.Body = "Hello"
router.Route(&buf, msg) router.Route(conn, msg)
if !bytes.Equal(buf.Bytes(), successFlag) { if conn.String() != successFlag {
t.Error("Message was not matched and routed properly") t.Error("Message was not matched and routed properly")
} }
// Check that an IQ packet is not matched // Check that an IQ packet is not matched
buf = bytes.Buffer{} conn = NewSenderMock()
iq := xmpp.NewIQ("get", "", "localhost", "1", "") iq := xmpp.NewIQ("get", "", "localhost", "1", "")
iq.Payload = append(iq.Payload, &xmpp.DiscoInfo{}) iq.Payload = append(iq.Payload, &xmpp.DiscoInfo{})
router.Route(&buf, iq) router.Route(conn, iq)
if bytes.Equal(buf.Bytes(), successFlag) { if conn.String() == successFlag {
t.Error("IQ should not have been matched and routed") t.Error("IQ should not have been matched and routed")
} }
} }
@ -41,12 +44,12 @@ func TestIQNSMatcher(t *testing.T) {
router := xmpp.NewRouter() router := xmpp.NewRouter()
router.NewRoute(). router.NewRoute().
IQNamespaces(xmpp.NSDiscoInfo, xmpp.NSDiscoItems). IQNamespaces(xmpp.NSDiscoInfo, xmpp.NSDiscoItems).
HandlerFunc(func(w io.Writer, p xmpp.Packet) { HandlerFunc(func(s xmpp.Sender, p xmpp.Packet) {
_, _ = w.Write(successFlag) _ = s.SendRaw(successFlag)
}) })
// Check that an IQ with proper namespace does match // Check that an IQ with proper namespace does match
var buf bytes.Buffer conn := NewSenderMock()
iqDisco := xmpp.NewIQ("get", "", "localhost", "1", "") iqDisco := xmpp.NewIQ("get", "", "localhost", "1", "")
// TODO: Add a function to generate payload with proper namespace initialisation // TODO: Add a function to generate payload with proper namespace initialisation
iqDisco.Payload = append(iqDisco.Payload, &xmpp.DiscoInfo{ iqDisco.Payload = append(iqDisco.Payload, &xmpp.DiscoInfo{
@ -54,13 +57,13 @@ func TestIQNSMatcher(t *testing.T) {
Space: xmpp.NSDiscoInfo, Space: xmpp.NSDiscoInfo,
Local: "query", Local: "query",
}}) }})
router.Route(&buf, iqDisco) router.Route(conn, iqDisco)
if !bytes.Equal(buf.Bytes(), successFlag) { if conn.String() != successFlag {
t.Errorf("IQ should have been matched and routed: %v", iqDisco) t.Errorf("IQ should have been matched and routed: %v", iqDisco)
} }
// Check that another namespace is not matched // Check that another namespace is not matched
buf = bytes.Buffer{} conn = NewSenderMock()
iqVersion := xmpp.NewIQ("get", "", "localhost", "1", "") iqVersion := xmpp.NewIQ("get", "", "localhost", "1", "")
// TODO: Add a function to generate payload with proper namespace initialisation // TODO: Add a function to generate payload with proper namespace initialisation
iqVersion.Payload = append(iqVersion.Payload, &xmpp.DiscoInfo{ iqVersion.Payload = append(iqVersion.Payload, &xmpp.DiscoInfo{
@ -68,8 +71,48 @@ func TestIQNSMatcher(t *testing.T) {
Space: "jabber:iq:version", Space: "jabber:iq:version",
Local: "query", Local: "query",
}}) }})
router.Route(&buf, iqVersion) router.Route(conn, iqVersion)
if bytes.Equal(buf.Bytes(), successFlag) { if conn.String() == successFlag {
t.Errorf("IQ should not have been matched and routed: %v", iqVersion) t.Errorf("IQ should not have been matched and routed: %v", iqVersion)
} }
} }
var successFlag = "matched"
type SenderMock struct {
buffer *bytes.Buffer
}
func NewSenderMock() SenderMock {
return SenderMock{buffer: new(bytes.Buffer)}
}
func (s SenderMock) Send(packet xmpp.Packet) error {
out, err := xml.Marshal(packet)
if err != nil {
return err
}
s.buffer.Write(out)
return nil
}
func (s SenderMock) SendRaw(str string) error {
s.buffer.WriteString(str)
return nil
}
func (s SenderMock) String() string {
return s.buffer.String()
}
func TestSenderMock(t *testing.T) {
conn := NewSenderMock()
msg := xmpp.NewMessage("", "", "test@localhost", "1", "")
msg.Body = "Hello"
if err := conn.Send(msg); err != nil {
t.Error("Could not send message")
}
if conn.String() != "<message id=\"1\" to=\"test@localhost\"><body>Hello</body></message>" {
t.Errorf("Incorrect packet sent: %s", conn.String())
}
}

View file

@ -19,12 +19,20 @@ import (
// permanent errors to avoid useless reconnection loops. // permanent errors to avoid useless reconnection loops.
// - Metrics processing // - Metrics processing
// StreamClient is an interface used by StreamManager to control Client lifecycle,
// set callback and trigger reconnection.
type StreamClient interface { type StreamClient interface {
Connect() error Connect() error
Disconnect() Disconnect()
SetHandler(handler EventHandler) SetHandler(handler EventHandler)
} }
// Sender is an interface provided by Stream clients to allow sending XMPP data.
type Sender interface {
Send(packet Packet) error
SendRaw(packet string) error
}
// StreamManager supervises an XMPP client connection. Its role is to handle connection events and // StreamManager supervises an XMPP client connection. Its role is to handle connection events and
// apply reconnection strategy. // apply reconnection strategy.
type StreamManager struct { type StreamManager struct {