Refactor tests
This commit is contained in:
parent
22ba8d1f4e
commit
e59a86c380
|
@ -99,7 +99,9 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9
|
|||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/processone/mpg123 v1.0.0 h1:o2WOyGZRM255or1Zc/LtF/jARn51B+9aQl72Qace0GA=
|
||||
github.com/processone/mpg123 v1.0.0/go.mod h1:X/FeL+h8vD1bYsG9tIWV3M2c4qNTZOficyvPVBP08go=
|
||||
github.com/processone/soundcloud v1.0.0 h1:/+i6+Yveb7Y6IFGDSkesYI+HddblzcRTQClazzVHxoE=
|
||||
github.com/processone/soundcloud v1.0.0/go.mod h1:kDLeWpkRtN3C8kIReQdxoiRi92P9xR6yW6qLOJnNWfY=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
|
@ -155,6 +157,7 @@ golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73r
|
|||
golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
|
|
@ -28,7 +28,7 @@ func main() {
|
|||
router := xmpp.NewRouter()
|
||||
router.HandleFunc("message", handleMessage)
|
||||
|
||||
client, err := xmpp.NewClient(config, router, errorHandler)
|
||||
client, err := xmpp.NewClient(&config, router, errorHandler)
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", err)
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ func main() {
|
|||
handleIQ(s, p, player)
|
||||
})
|
||||
|
||||
client, err := xmpp.NewClient(config, router, errorHandler)
|
||||
client, err := xmpp.NewClient(&config, router, errorHandler)
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", err)
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ func main() {
|
|||
router := xmpp.NewRouter()
|
||||
router.HandleFunc("message", handleMessage)
|
||||
|
||||
client, err := xmpp.NewClient(config, router, errorHandler)
|
||||
client, err := xmpp.NewClient(&config, router, errorHandler)
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", err)
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ func main() {
|
|||
log.Println("Received a message ! => \n" + string(data))
|
||||
})
|
||||
|
||||
client, err := xmpp.NewClient(config, router, func(err error) { log.Println(err) })
|
||||
client, err := xmpp.NewClient(&config, router, func(err error) { log.Println(err) })
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", err)
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ func main() {
|
|||
router := xmpp.NewRouter()
|
||||
router.HandleFunc("message", handleMessage)
|
||||
|
||||
client, err := xmpp.NewClient(config, router, errorHandler)
|
||||
client, err := xmpp.NewClient(&config, router, errorHandler)
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", err)
|
||||
}
|
||||
|
|
13
auth.go
13
auth.go
|
@ -60,10 +60,21 @@ func authPlain(socket io.ReadWriter, decoder *xml.Decoder, mech string, user str
|
|||
raw := "\x00" + user + "\x00" + secret
|
||||
enc := make([]byte, base64.StdEncoding.EncodedLen(len(raw)))
|
||||
base64.StdEncoding.Encode(enc, []byte(raw))
|
||||
_, err := fmt.Fprintf(socket, "<auth xmlns='%s' mechanism='%s'>%s</auth>", stanza.NSSASL, mech, enc)
|
||||
|
||||
a := stanza.SASLAuth{
|
||||
Mechanism: mech,
|
||||
Value: string(enc),
|
||||
}
|
||||
data, err := xml.Marshal(a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := socket.Write(data)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if n == 0 {
|
||||
return errors.New("failed to write authSASL nonza to socket : wrote 0 bytes")
|
||||
}
|
||||
|
||||
// Next message should be either success or failure.
|
||||
val, err := stanza.NextPacket(decoder)
|
||||
|
|
161
client.go
161
client.go
|
@ -6,6 +6,7 @@ import (
|
|||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gosrc.io/xmpp/stanza"
|
||||
|
@ -14,15 +15,36 @@ import (
|
|||
//=============================================================================
|
||||
// EventManager
|
||||
|
||||
// ConnState represents the current connection state.
|
||||
// SyncConnState represents the current connection state.
|
||||
type SyncConnState struct {
|
||||
sync.RWMutex
|
||||
// Current state of the client. Please use the dedicated getter and setter for this field as they are thread safe.
|
||||
state ConnState
|
||||
}
|
||||
type ConnState = uint8
|
||||
|
||||
// getState is a thread-safe getter for the current state
|
||||
func (scs *SyncConnState) getState() ConnState {
|
||||
var res ConnState
|
||||
scs.RLock()
|
||||
res = scs.state
|
||||
scs.RUnlock()
|
||||
return res
|
||||
}
|
||||
|
||||
// setState is a thread-safe setter for the current
|
||||
func (scs *SyncConnState) setState(cs ConnState) {
|
||||
scs.Lock()
|
||||
scs.state = cs
|
||||
scs.Unlock()
|
||||
}
|
||||
|
||||
// This is a the list of events happening on the connection that the
|
||||
// client can be notified about.
|
||||
const (
|
||||
InitialPresence = "<presence/>"
|
||||
StateDisconnected ConnState = iota
|
||||
StateConnected
|
||||
StateResuming
|
||||
StateSessionEstablished
|
||||
StateStreamError
|
||||
StatePermanentError
|
||||
|
@ -31,7 +53,7 @@ const (
|
|||
// Event is a structure use to convey event changes related to client state. This
|
||||
// is for example used to notify the client when the client get disconnected.
|
||||
type Event struct {
|
||||
State ConnState
|
||||
State SyncConnState
|
||||
Description string
|
||||
StreamError string
|
||||
SMState SMState
|
||||
|
@ -44,7 +66,16 @@ type SMState struct {
|
|||
Id string
|
||||
// Inbound stanza count
|
||||
Inbound uint
|
||||
// TODO Store location for IP affinity
|
||||
|
||||
// IP affinity
|
||||
preferredReconAddr string
|
||||
|
||||
// Error
|
||||
StreamErrorGroup stanza.StanzaErrorGroup
|
||||
|
||||
// Track sent stanzas
|
||||
*stanza.UnAckQueue
|
||||
|
||||
// TODO Store max and timestamp, to check if we should retry resumption or not
|
||||
}
|
||||
|
||||
|
@ -53,29 +84,35 @@ type SMState struct {
|
|||
type EventHandler func(Event) error
|
||||
|
||||
type EventManager struct {
|
||||
// Store current state
|
||||
CurrentState ConnState
|
||||
// Store current state. Please use "getState" and "setState" to access and/or modify this.
|
||||
CurrentState SyncConnState
|
||||
|
||||
// Callback used to propagate connection state changes
|
||||
Handler EventHandler
|
||||
}
|
||||
|
||||
// updateState changes the CurrentState in the event manager. The state read is threadsafe but there is no guarantee
|
||||
// regarding the triggered callback function.
|
||||
func (em *EventManager) updateState(state ConnState) {
|
||||
em.CurrentState = state
|
||||
em.CurrentState.setState(state)
|
||||
if em.Handler != nil {
|
||||
em.Handler(Event{State: em.CurrentState})
|
||||
}
|
||||
}
|
||||
|
||||
// disconnected changes the CurrentState in the event manager to "disconnected". The state read is threadsafe but there is no guarantee
|
||||
// regarding the triggered callback function.
|
||||
func (em *EventManager) disconnected(state SMState) {
|
||||
em.CurrentState = StateDisconnected
|
||||
em.CurrentState.setState(StateDisconnected)
|
||||
if em.Handler != nil {
|
||||
em.Handler(Event{State: em.CurrentState, SMState: state})
|
||||
}
|
||||
}
|
||||
|
||||
// streamError changes the CurrentState in the event manager to "streamError". The state read is threadsafe but there is no guarantee
|
||||
// regarding the triggered callback function.
|
||||
func (em *EventManager) streamError(error, desc string) {
|
||||
em.CurrentState = StateStreamError
|
||||
em.CurrentState.setState(StateStreamError)
|
||||
if em.Handler != nil {
|
||||
em.Handler(Event{State: em.CurrentState, StreamError: error, Description: desc})
|
||||
}
|
||||
|
@ -90,7 +127,7 @@ var ErrCanOnlySendGetOrSetIq = errors.New("SendIQ can only send get and set IQ s
|
|||
// server.
|
||||
type Client struct {
|
||||
// Store user defined options and states
|
||||
config Config
|
||||
config *Config
|
||||
// Session gather data that can be accessed by users of this library
|
||||
Session *Session
|
||||
transport Transport
|
||||
|
@ -100,6 +137,12 @@ type Client struct {
|
|||
EventManager
|
||||
// Handle errors from client execution
|
||||
ErrorHandler func(error)
|
||||
|
||||
// Post connection hook. This will be executed on first connection
|
||||
PostFirstConnHook func() error
|
||||
|
||||
// Post resume hook. This will be executed after the client resumes a lost connection using StreamManagement (XEP-0198)
|
||||
PostReconnectHook func() error
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -107,9 +150,9 @@ Setting up the client / Checking the parameters
|
|||
*/
|
||||
|
||||
// NewClient generates a new XMPP client, based on Config passed as parameters.
|
||||
// If host is not specified, the DNS SRV should be used to find the host from the domainpart of the Jid.
|
||||
// If host is not specified, the DNS SRV should be used to find the host from the domain part of the Jid.
|
||||
// Default the port to 5222.
|
||||
func NewClient(config Config, r *Router, errorHandler func(error)) (c *Client, err error) {
|
||||
func NewClient(config *Config, r *Router, errorHandler func(error)) (c *Client, err error) {
|
||||
if config.KeepaliveInterval == 0 {
|
||||
config.KeepaliveInterval = time.Second * 30
|
||||
}
|
||||
|
@ -169,26 +212,45 @@ func NewClient(config Config, r *Router, errorHandler func(error)) (c *Client, e
|
|||
return
|
||||
}
|
||||
|
||||
// Connect triggers actual TCP connection, based on previously defined parameters.
|
||||
// Connect simply triggers resumption, with an empty session state.
|
||||
// Connect establishes a first time connection to a XMPP server.
|
||||
// It calls the PostFirstConnHook
|
||||
func (c *Client) Connect() error {
|
||||
var state SMState
|
||||
return c.Resume(state)
|
||||
err := c.connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO: Do we always want to send initial presence automatically ?
|
||||
// Do we need an option to avoid that or do we rely on client to send the presence itself ?
|
||||
err = c.sendWithWriter(c.transport, []byte(InitialPresence))
|
||||
// Execute the post first connection hook. Typically this holds "ask for roster" and this type of actions.
|
||||
if c.PostFirstConnHook != nil {
|
||||
err = c.PostFirstConnHook()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Start the keepalive go routine
|
||||
keepaliveQuit := make(chan struct{})
|
||||
go keepalive(c.transport, c.config.KeepaliveInterval, keepaliveQuit)
|
||||
// Start the receiver go routine
|
||||
go c.recv(keepaliveQuit)
|
||||
return err
|
||||
}
|
||||
|
||||
// Resume attempts resuming a Stream Managed session, based on the provided stream management
|
||||
// state.
|
||||
func (c *Client) Resume(state SMState) error {
|
||||
// connect establishes an actual TCP connection, based on previously defined parameters, as well as a XMPP session
|
||||
func (c *Client) connect() error {
|
||||
var state SMState
|
||||
var err error
|
||||
|
||||
// This is the TCP connection
|
||||
streamId, err := c.transport.Connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.updateState(StateConnected)
|
||||
|
||||
// Client is ok, we now open XMPP session
|
||||
if c.Session, err = NewSession(c.transport, c.config, state); err != nil {
|
||||
// Client is ok, we now open XMPP session with TLS negotiation if possible and session resume or binding
|
||||
// depending on state.
|
||||
if c.Session, err = NewSession(c, state); err != nil {
|
||||
// Try to get the stream close tag from the server.
|
||||
go func() {
|
||||
for {
|
||||
|
@ -212,22 +274,26 @@ func (c *Client) Resume(state SMState) error {
|
|||
c.Session.StreamId = streamId
|
||||
c.updateState(StateSessionEstablished)
|
||||
|
||||
// Start the keepalive go routine
|
||||
keepaliveQuit := make(chan struct{})
|
||||
go keepalive(c.transport, c.config.KeepaliveInterval, keepaliveQuit)
|
||||
// Start the receiver go routine
|
||||
state = c.Session.SMState
|
||||
go c.recv(state, keepaliveQuit)
|
||||
|
||||
// We're connected and can now receive and send messages.
|
||||
//fmt.Fprintf(client.conn, "<presence xml:lang='en'><show>%s</show><status>%s</status></presence>", "chat", "Online")
|
||||
// TODO: Do we always want to send initial presence automatically ?
|
||||
// Do we need an option to avoid that or do we rely on client to send the presence itself ?
|
||||
err = c.sendWithWriter(c.transport, []byte(InitialPresence))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Resume attempts resuming a Stream Managed session, based on the provided stream management
|
||||
// state. See XEP-0198
|
||||
func (c *Client) Resume() error {
|
||||
c.EventManager.updateState(StateResuming)
|
||||
err := c.connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Execute post reconnect hook. This can be different from the first connection hook, and not trigger roster retrival
|
||||
// for example.
|
||||
if c.PostReconnectHook != nil {
|
||||
err = c.PostReconnectHook()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Disconnect disconnects the client from the server, sending a stream close nonza and closing the TCP connection.
|
||||
func (c *Client) Disconnect() error {
|
||||
if c.transport != nil {
|
||||
return c.transport.Close()
|
||||
|
@ -252,6 +318,15 @@ func (c *Client) Send(packet stanza.Packet) error {
|
|||
return errors.New("cannot marshal packet " + err.Error())
|
||||
}
|
||||
|
||||
// Store stanza as non-acked as part of stream management
|
||||
// See https://xmpp.org/extensions/xep-0198.html#scenarios
|
||||
if c.config.StreamManagementEnable {
|
||||
if _, ok := packet.(stanza.SMRequest); !ok {
|
||||
toStore := stanza.UnAckedStz{Stz: string(data)}
|
||||
c.Session.SMState.UnAckQueue.Push(&toStore)
|
||||
}
|
||||
}
|
||||
|
||||
return c.sendWithWriter(c.transport, data)
|
||||
}
|
||||
|
||||
|
@ -284,6 +359,12 @@ func (c *Client) SendRaw(packet string) error {
|
|||
return errors.New("client is not connected")
|
||||
}
|
||||
|
||||
// Store stanza as non-acked as part of stream management
|
||||
// See https://xmpp.org/extensions/xep-0198.html#scenarios
|
||||
if c.config.StreamManagementEnable {
|
||||
toStore := stanza.UnAckedStz{Stz: packet}
|
||||
c.Session.SMState.UnAckQueue.Push(&toStore)
|
||||
}
|
||||
return c.sendWithWriter(c.transport, []byte(packet))
|
||||
}
|
||||
|
||||
|
@ -297,13 +378,13 @@ func (c *Client) sendWithWriter(writer io.Writer, packet []byte) error {
|
|||
// Go routines
|
||||
|
||||
// Loop: Receive data from server
|
||||
func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}) {
|
||||
func (c *Client) recv(keepaliveQuit chan<- struct{}) {
|
||||
for {
|
||||
val, err := stanza.NextPacket(c.transport.GetDecoder())
|
||||
if err != nil {
|
||||
c.ErrorHandler(err)
|
||||
close(keepaliveQuit)
|
||||
c.disconnected(state)
|
||||
c.disconnected(c.Session.SMState)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -321,7 +402,7 @@ func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}) {
|
|||
answer := stanza.SMAnswer{XMLName: xml.Name{
|
||||
Space: stanza.NSStreamManagement,
|
||||
Local: "a",
|
||||
}, H: state.Inbound}
|
||||
}, H: c.Session.SMState.Inbound}
|
||||
err = c.Send(answer)
|
||||
if err != nil {
|
||||
c.ErrorHandler(err)
|
||||
|
@ -332,7 +413,7 @@ func (c *Client) recv(state SMState, keepaliveQuit chan<- struct{}) {
|
|||
c.transport.ReceivedStreamClose()
|
||||
return
|
||||
default:
|
||||
state.Inbound++
|
||||
c.Session.SMState.Inbound++
|
||||
}
|
||||
// Do normal route processing in a go-routine so we can immediately
|
||||
// start receiving other stanzas. This also allows route handlers to
|
||||
|
|
|
@ -2,7 +2,16 @@ package xmpp
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"gosrc.io/xmpp/stanza"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
streamManagementID = "test-stream_management-id"
|
||||
)
|
||||
|
||||
func TestClient_Send(t *testing.T) {
|
||||
|
@ -17,3 +26,554 @@ func TestClient_Send(t *testing.T) {
|
|||
t.Errorf("Incorrect value sent to buffer: '%s'", buffer.String())
|
||||
}
|
||||
}
|
||||
|
||||
// Stream management test.
|
||||
// Connection is established, then the server sends supported features and so on.
|
||||
// After the bind, client attempts a stream management enablement, and server replies in kind.
|
||||
func Test_StreamManagement(t *testing.T) {
|
||||
serverDone := make(chan struct{})
|
||||
clientDone := make(chan struct{})
|
||||
|
||||
client, mock := initSrvCliForResumeTests(t, func(t *testing.T, sc *ServerConn) {
|
||||
checkClientOpenStream(t, sc)
|
||||
|
||||
sendStreamFeatures(t, sc) // Send initial features
|
||||
readAuth(t, sc.decoder)
|
||||
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||
|
||||
checkClientOpenStream(t, sc) // Reset stream
|
||||
sendFeaturesStreamManagment(t, sc) // Send post auth features
|
||||
bind(t, sc)
|
||||
enableStreamManagement(t, sc, false, true)
|
||||
serverDone <- struct{}{}
|
||||
}, testClientStreamManagement, true, true)
|
||||
go func() {
|
||||
var state SMState
|
||||
var err error
|
||||
// Client is ok, we now open XMPP session
|
||||
if client.Session, err = NewSession(client, state); err != nil {
|
||||
t.Fatalf("failed to open XMPP session: %s", err)
|
||||
}
|
||||
clientDone <- struct{}{}
|
||||
}()
|
||||
|
||||
waitForEntity(t, clientDone)
|
||||
waitForEntity(t, serverDone)
|
||||
mock.Stop()
|
||||
}
|
||||
|
||||
// Absence of stream management test.
|
||||
// Connection is established, then the server sends supported features and so on.
|
||||
// Client has stream management disabled in its config, and should not ask for it. Server is not set up to reply.
|
||||
func Test_NoStreamManagement(t *testing.T) {
|
||||
serverDone := make(chan struct{})
|
||||
clientDone := make(chan struct{})
|
||||
|
||||
// Setup Mock server
|
||||
client, mock := initSrvCliForResumeTests(t, func(t *testing.T, sc *ServerConn) {
|
||||
checkClientOpenStream(t, sc)
|
||||
|
||||
sendStreamFeatures(t, sc) // Send initial features
|
||||
readAuth(t, sc.decoder)
|
||||
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||
|
||||
checkClientOpenStream(t, sc) // Reset stream
|
||||
sendFeaturesNoStreamManagment(t, sc) // Send post auth features
|
||||
bind(t, sc)
|
||||
serverDone <- struct{}{}
|
||||
}, testClientStreamManagement, true, false)
|
||||
|
||||
go func() {
|
||||
var state SMState
|
||||
|
||||
// Client is ok, we now open XMPP session
|
||||
var err error
|
||||
if client.Session, err = NewSession(client, state); err != nil {
|
||||
t.Fatalf("failed to open XMPP session: %s", err)
|
||||
}
|
||||
clientDone <- struct{}{}
|
||||
}()
|
||||
|
||||
waitForEntity(t, clientDone)
|
||||
waitForEntity(t, serverDone)
|
||||
|
||||
mock.Stop()
|
||||
}
|
||||
|
||||
func Test_StreamManagementNotSupported(t *testing.T) {
|
||||
serverDone := make(chan struct{})
|
||||
clientDone := make(chan struct{})
|
||||
|
||||
client, mock := initSrvCliForResumeTests(t, func(t *testing.T, sc *ServerConn) {
|
||||
checkClientOpenStream(t, sc)
|
||||
|
||||
sendStreamFeatures(t, sc) // Send initial features
|
||||
readAuth(t, sc.decoder)
|
||||
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||
|
||||
checkClientOpenStream(t, sc) // Reset stream
|
||||
sendFeaturesNoStreamManagment(t, sc) // Send post auth features
|
||||
bind(t, sc)
|
||||
serverDone <- struct{}{}
|
||||
}, testClientStreamManagement, true, true)
|
||||
|
||||
go func() {
|
||||
var state SMState
|
||||
var err error
|
||||
// Client is ok, we now open XMPP session
|
||||
if client.Session, err = NewSession(client, state); err != nil {
|
||||
t.Fatalf("failed to open XMPP session: %s", err)
|
||||
}
|
||||
clientDone <- struct{}{}
|
||||
}()
|
||||
|
||||
// Wait for client
|
||||
waitForEntity(t, clientDone)
|
||||
|
||||
// Check if client got a positive stream management response from the server
|
||||
if client.Session.Features.DoesStreamManagement() {
|
||||
t.Fatalf("server does not provide stream management")
|
||||
}
|
||||
|
||||
// Wait for server
|
||||
waitForEntity(t, serverDone)
|
||||
mock.Stop()
|
||||
}
|
||||
|
||||
func Test_StreamManagementNoResume(t *testing.T) {
|
||||
serverDone := make(chan struct{})
|
||||
clientDone := make(chan struct{})
|
||||
|
||||
client, mock := initSrvCliForResumeTests(t, func(t *testing.T, sc *ServerConn) {
|
||||
checkClientOpenStream(t, sc)
|
||||
|
||||
sendStreamFeatures(t, sc) // Send initial features
|
||||
readAuth(t, sc.decoder)
|
||||
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||
|
||||
checkClientOpenStream(t, sc) // Reset stream
|
||||
sendFeaturesStreamManagment(t, sc) // Send post auth features
|
||||
bind(t, sc)
|
||||
enableStreamManagement(t, sc, false, false)
|
||||
serverDone <- struct{}{}
|
||||
}, testClientStreamManagement, true, true)
|
||||
|
||||
go func() {
|
||||
var state SMState
|
||||
var err error
|
||||
// Client is ok, we now open XMPP session
|
||||
if client.Session, err = NewSession(client, state); err != nil {
|
||||
t.Fatalf("failed to open XMPP session: %s", err)
|
||||
}
|
||||
clientDone <- struct{}{}
|
||||
}()
|
||||
waitForEntity(t, clientDone)
|
||||
if IsStreamResumable(client) {
|
||||
t.Fatalf("server does not support resumption but client says stream is resumable")
|
||||
}
|
||||
waitForEntity(t, serverDone)
|
||||
mock.Stop()
|
||||
}
|
||||
|
||||
func Test_StreamManagementResume(t *testing.T) {
|
||||
// Setup Mock server
|
||||
mock := ServerMock{}
|
||||
mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
|
||||
checkClientOpenStream(t, sc)
|
||||
|
||||
sendStreamFeatures(t, sc) // Send initial features
|
||||
readAuth(t, sc.decoder)
|
||||
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||
|
||||
checkClientOpenStream(t, sc) // Reset stream
|
||||
sendFeaturesStreamManagment(t, sc) // Send post auth features
|
||||
bind(t, sc)
|
||||
enableStreamManagement(t, sc, false, true)
|
||||
discardPresence(t, sc)
|
||||
})
|
||||
|
||||
// Test / Check result
|
||||
config := Config{
|
||||
TransportConfiguration: TransportConfiguration{
|
||||
Address: testXMPPAddress,
|
||||
},
|
||||
Jid: "test@localhost",
|
||||
Credential: Password("test"),
|
||||
Insecure: true,
|
||||
StreamManagementEnable: true,
|
||||
streamManagementResume: true} // Enable stream management
|
||||
|
||||
var client *Client
|
||||
router := NewRouter()
|
||||
client, err := NewClient(&config, router, clientDefaultErrorHandler)
|
||||
if err != nil {
|
||||
t.Errorf("connect create XMPP client: %s", err)
|
||||
}
|
||||
|
||||
err = client.Connect()
|
||||
if err != nil {
|
||||
t.Fatalf("could not connect client to mock server: %s", err)
|
||||
}
|
||||
|
||||
statusCorrectChan := make(chan struct{})
|
||||
kill := make(chan struct{})
|
||||
|
||||
transp, ok := client.transport.(*XMPPTransport)
|
||||
if !ok {
|
||||
t.Fatalf("problem with client transport ")
|
||||
}
|
||||
|
||||
transp.conn.Close()
|
||||
mock.Stop()
|
||||
|
||||
// Check if status is correctly updated because of the disconnect
|
||||
go checkClientResumeStatus(client, statusCorrectChan, kill)
|
||||
select {
|
||||
case <-statusCorrectChan:
|
||||
// Test passed
|
||||
case <-time.After(5 * time.Second):
|
||||
kill <- struct{}{}
|
||||
t.Fatalf("Client is not in disconnected state while it should be. Timed out")
|
||||
}
|
||||
|
||||
// Check if the client can have its connection resumed using its state but also its configuration
|
||||
if !IsStreamResumable(client) {
|
||||
t.Fatalf("should support resumption")
|
||||
}
|
||||
|
||||
// Reboot server. We need to make a new one because (at least for now) the mock server can only have one handler
|
||||
// and they should be different between a first connection and a stream resume since exchanged messages
|
||||
// are different (See XEP-0198)
|
||||
mock2 := ServerMock{}
|
||||
mock2.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
|
||||
// Reconnect
|
||||
checkClientOpenStream(t, sc)
|
||||
|
||||
sendStreamFeatures(t, sc) // Send initial features
|
||||
readAuth(t, sc.decoder)
|
||||
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||
|
||||
checkClientOpenStream(t, sc) // Reset stream
|
||||
sendFeaturesStreamManagment(t, sc) // Send post auth features
|
||||
resumeStream(t, sc)
|
||||
})
|
||||
|
||||
// Reconnect
|
||||
err = client.Resume()
|
||||
if err != nil {
|
||||
t.Fatalf("could not connect client to mock server: %s", err)
|
||||
}
|
||||
mock2.Stop()
|
||||
}
|
||||
|
||||
func Test_StreamManagementFail(t *testing.T) {
|
||||
// Setup Mock server
|
||||
mock := ServerMock{}
|
||||
mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
|
||||
checkClientOpenStream(t, sc)
|
||||
|
||||
sendStreamFeatures(t, sc) // Send initial features
|
||||
readAuth(t, sc.decoder)
|
||||
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||
|
||||
checkClientOpenStream(t, sc) // Reset stream
|
||||
sendFeaturesStreamManagment(t, sc) // Send post auth features
|
||||
bind(t, sc)
|
||||
enableStreamManagement(t, sc, true, true)
|
||||
})
|
||||
|
||||
// Test / Check result
|
||||
config := Config{
|
||||
TransportConfiguration: TransportConfiguration{
|
||||
Address: testXMPPAddress,
|
||||
},
|
||||
Jid: "test@localhost",
|
||||
Credential: Password("test"),
|
||||
Insecure: true,
|
||||
StreamManagementEnable: true,
|
||||
streamManagementResume: true} // Enable stream management
|
||||
|
||||
var client *Client
|
||||
router := NewRouter()
|
||||
client, err := NewClient(&config, router, clientDefaultErrorHandler)
|
||||
if err != nil {
|
||||
t.Errorf("connect create XMPP client: %s", err)
|
||||
}
|
||||
|
||||
var state SMState
|
||||
_, err = client.transport.Connect()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Client is ok, we now open XMPP session
|
||||
if client.Session, err = NewSession(client, state); err == nil {
|
||||
t.Fatalf("test is supposed to err")
|
||||
}
|
||||
if client.Session.SMState.StreamErrorGroup == nil {
|
||||
t.Fatalf("error was not stored correctly in session state")
|
||||
}
|
||||
|
||||
mock.Stop()
|
||||
}
|
||||
|
||||
func Test_SendStanzaQueueWithSM(t *testing.T) {
|
||||
// Setup Mock server
|
||||
mock := ServerMock{}
|
||||
serverDone := make(chan struct{})
|
||||
mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
|
||||
checkClientOpenStream(t, sc)
|
||||
|
||||
sendStreamFeatures(t, sc) // Send initial features
|
||||
readAuth(t, sc.decoder)
|
||||
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||
|
||||
checkClientOpenStream(t, sc) // Reset stream
|
||||
sendFeaturesStreamManagment(t, sc) // Send post auth features
|
||||
bind(t, sc)
|
||||
enableStreamManagement(t, sc, false, true)
|
||||
|
||||
// Ignore the initial presence sent to the server by the client so we can move on to the next packet.
|
||||
discardPresence(t, sc)
|
||||
|
||||
// Used here to silently discard the IQ sent by the client, in order to later trigger a resend
|
||||
skipPacket(t, sc)
|
||||
// Respond to the client ACK request with a number of processed stanzas of 0. This should trigger a resend
|
||||
// of previously ignored stanza to the server, which this handler element will be expecting.
|
||||
respondWithAck(t, sc, 0, serverDone)
|
||||
})
|
||||
|
||||
// Test / Check result
|
||||
config := Config{
|
||||
TransportConfiguration: TransportConfiguration{
|
||||
Address: testXMPPAddress,
|
||||
},
|
||||
Jid: "test@localhost",
|
||||
Credential: Password("test"),
|
||||
Insecure: true,
|
||||
StreamManagementEnable: true,
|
||||
streamManagementResume: true} // Enable stream management
|
||||
|
||||
var client *Client
|
||||
router := NewRouter()
|
||||
client, err := NewClient(&config, router, clientDefaultErrorHandler)
|
||||
if err != nil {
|
||||
t.Errorf("connect create XMPP client: %s", err)
|
||||
}
|
||||
|
||||
err = client.Connect()
|
||||
|
||||
client.SendRaw(`<iq id='ls72g593' type='get'>
|
||||
<query xmlns='jabber:iq:roster'/>
|
||||
</iq>
|
||||
`)
|
||||
|
||||
// Last stanza was discarded silently by the server. Let's ask an ack for it. This should trigger resend as the server
|
||||
// will respond with an acknowledged number of stanzas of 0.
|
||||
r := stanza.SMRequest{}
|
||||
client.Send(r)
|
||||
|
||||
select {
|
||||
case <-time.After(defaultChannelTimeout):
|
||||
t.Fatalf("server failed to complete the test in time")
|
||||
case <-serverDone:
|
||||
// Test completed successfully
|
||||
}
|
||||
|
||||
mock.Stop()
|
||||
}
|
||||
|
||||
//========================================================================
|
||||
// Helper functions for tests
|
||||
|
||||
func skipPacket(t *testing.T, sc *ServerConn) {
|
||||
var p stanza.IQ
|
||||
se, err := stanza.NextStart(sc.decoder)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("cannot read packet: %s", err)
|
||||
return
|
||||
}
|
||||
if err := sc.decoder.DecodeElement(&p, &se); err != nil {
|
||||
t.Fatalf("cannot decode packet: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func respondWithAck(t *testing.T, sc *ServerConn, h int, serverDone chan struct{}) {
|
||||
|
||||
// Mock server reads the ack request
|
||||
var p stanza.SMRequest
|
||||
se, err := stanza.NextStart(sc.decoder)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("cannot read packet: %s", err)
|
||||
return
|
||||
}
|
||||
if err := sc.decoder.DecodeElement(&p, &se); err != nil {
|
||||
t.Fatalf("cannot decode packet: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Mock server sends the ack response
|
||||
a := stanza.SMAnswer{
|
||||
H: uint(h),
|
||||
}
|
||||
data, err := xml.Marshal(a)
|
||||
_, err = sc.connection.Write(data)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to send response ack")
|
||||
}
|
||||
|
||||
// Mock server reads the re-sent stanza that was previously discarded intentionally
|
||||
var p2 stanza.IQ
|
||||
nse, err := stanza.NextStart(sc.decoder)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("cannot read packet: %s", err)
|
||||
return
|
||||
}
|
||||
if err := sc.decoder.DecodeElement(&p2, &nse); err != nil {
|
||||
t.Fatalf("cannot decode packet: %s", err)
|
||||
return
|
||||
}
|
||||
serverDone <- struct{}{}
|
||||
}
|
||||
|
||||
func sendFeaturesStreamManagment(t *testing.T, sc *ServerConn) {
|
||||
// This is a basic server, supporting only 2 features after auth: stream management & session binding
|
||||
features := `<stream:features>
|
||||
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>
|
||||
<sm xmlns='urn:xmpp:sm:3'/>
|
||||
</stream:features>`
|
||||
if _, err := fmt.Fprintln(sc.connection, features); err != nil {
|
||||
t.Fatalf("cannot send stream feature: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func sendFeaturesNoStreamManagment(t *testing.T, sc *ServerConn) {
|
||||
// This is a basic server, supporting only 2 features after auth: stream management & session binding
|
||||
features := `<stream:features>
|
||||
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>
|
||||
</stream:features>`
|
||||
if _, err := fmt.Fprintln(sc.connection, features); err != nil {
|
||||
t.Fatalf("cannot send stream feature: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// enableStreamManagement is a function for the mock server that can either mock a successful session, or fail depending on
|
||||
// the value of the "fail" boolean. True means the session should fail.
|
||||
func enableStreamManagement(t *testing.T, sc *ServerConn, fail bool, resume bool) {
|
||||
// Decode element into pointer storage
|
||||
var ed stanza.SMEnable
|
||||
se, err := stanza.NextStart(sc.decoder)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("cannot read stream management enable: %s", err)
|
||||
return
|
||||
}
|
||||
if err := sc.decoder.DecodeElement(&ed, &se); err != nil {
|
||||
t.Fatalf("cannot decode stream management enable: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if fail {
|
||||
f := stanza.SMFailed{
|
||||
H: nil,
|
||||
StreamErrorGroup: &stanza.UnexpectedRequest{},
|
||||
}
|
||||
data, err := xml.Marshal(f)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshall error response: %s", err)
|
||||
}
|
||||
sc.connection.Write(data)
|
||||
} else {
|
||||
e := &stanza.SMEnabled{
|
||||
Resume: strconv.FormatBool(resume),
|
||||
Id: streamManagementID,
|
||||
}
|
||||
data, err := xml.Marshal(e)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshall error response: %s", err)
|
||||
}
|
||||
sc.connection.Write(data)
|
||||
}
|
||||
}
|
||||
|
||||
func resumeStream(t *testing.T, sc *ServerConn) {
|
||||
h := uint(0)
|
||||
response := stanza.SMResumed{
|
||||
PrevId: streamManagementID,
|
||||
H: &h,
|
||||
}
|
||||
|
||||
data, err := xml.Marshal(response)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshall stream management enabled response : %s", err)
|
||||
}
|
||||
|
||||
writtenChan := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
sc.connection.Write(data)
|
||||
writtenChan <- struct{}{}
|
||||
}()
|
||||
select {
|
||||
case <-writtenChan:
|
||||
// We're done here
|
||||
return
|
||||
case <-time.After(defaultTimeout):
|
||||
t.Fatalf("failed to write enabled nonza to client")
|
||||
}
|
||||
}
|
||||
|
||||
func checkClientResumeStatus(client *Client, statusCorrectChan chan struct{}, killChan chan struct{}) {
|
||||
for {
|
||||
if client.CurrentState.getState() == StateDisconnected {
|
||||
statusCorrectChan <- struct{}{}
|
||||
}
|
||||
select {
|
||||
case <-killChan:
|
||||
return
|
||||
case <-time.After(time.Millisecond * 10):
|
||||
// Keep checking status value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func initSrvCliForResumeTests(t *testing.T, serverHandler func(*testing.T, *ServerConn), port int, StreamManagementEnable, StreamManagementResume bool) (*Client, *ServerMock) {
|
||||
mock := &ServerMock{}
|
||||
testServerAddress := fmt.Sprintf("%s:%d", testClientDomain, port)
|
||||
|
||||
mock.Start(t, testServerAddress, serverHandler)
|
||||
config := Config{
|
||||
TransportConfiguration: TransportConfiguration{
|
||||
Address: testServerAddress,
|
||||
},
|
||||
Jid: "test@localhost",
|
||||
Credential: Password("test"),
|
||||
Insecure: true,
|
||||
StreamManagementEnable: StreamManagementEnable,
|
||||
streamManagementResume: StreamManagementResume}
|
||||
|
||||
var client *Client
|
||||
var err error
|
||||
router := NewRouter()
|
||||
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
|
||||
t.Fatalf("connect create XMPP client: %s", err)
|
||||
}
|
||||
|
||||
if _, err = client.transport.Connect(); err != nil {
|
||||
t.Fatalf("XMPP connection failed: %s", err)
|
||||
}
|
||||
|
||||
return client, mock
|
||||
}
|
||||
|
||||
func waitForEntity(t *testing.T, entityDone chan struct{}) {
|
||||
select {
|
||||
case <-entityDone:
|
||||
case <-time.After(defaultTimeout):
|
||||
t.Fatalf("test timed out")
|
||||
}
|
||||
}
|
||||
|
|
182
client_test.go
182
client_test.go
|
@ -20,18 +20,20 @@ const (
|
|||
|
||||
func TestEventManager(t *testing.T) {
|
||||
mgr := EventManager{}
|
||||
mgr.updateState(StateConnected)
|
||||
if mgr.CurrentState != StateConnected {
|
||||
mgr.updateState(StateResuming)
|
||||
if mgr.CurrentState.getState() != StateResuming {
|
||||
t.Fatal("CurrentState not updated by updateState()")
|
||||
}
|
||||
|
||||
mgr.disconnected(SMState{})
|
||||
if mgr.CurrentState != StateDisconnected {
|
||||
|
||||
if mgr.CurrentState.getState() != StateDisconnected {
|
||||
t.Fatalf("CurrentState not reset by disconnected()")
|
||||
}
|
||||
|
||||
mgr.streamError(ErrTLSNotSupported.Error(), "")
|
||||
if mgr.CurrentState != StateStreamError {
|
||||
|
||||
if mgr.CurrentState.getState() != StateStreamError {
|
||||
t.Fatalf("CurrentState not set by streamError()")
|
||||
}
|
||||
}
|
||||
|
@ -53,7 +55,7 @@ func TestClient_Connect(t *testing.T) {
|
|||
var client *Client
|
||||
var err error
|
||||
router := NewRouter()
|
||||
if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil {
|
||||
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
|
||||
t.Errorf("connect create XMPP client: %s", err)
|
||||
}
|
||||
|
||||
|
@ -84,7 +86,7 @@ func TestClient_NoInsecure(t *testing.T) {
|
|||
var client *Client
|
||||
var err error
|
||||
router := NewRouter()
|
||||
if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil {
|
||||
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
|
||||
t.Errorf("cannot create XMPP client: %s", err)
|
||||
}
|
||||
|
||||
|
@ -117,7 +119,7 @@ func TestClient_FeaturesTracking(t *testing.T) {
|
|||
var client *Client
|
||||
var err error
|
||||
router := NewRouter()
|
||||
if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil {
|
||||
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
|
||||
t.Errorf("cannot create XMPP client: %s", err)
|
||||
}
|
||||
|
||||
|
@ -147,7 +149,7 @@ func TestClient_RFC3921Session(t *testing.T) {
|
|||
var client *Client
|
||||
var err error
|
||||
router := NewRouter()
|
||||
if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil {
|
||||
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
|
||||
t.Errorf("connect create XMPP client: %s", err)
|
||||
}
|
||||
|
||||
|
@ -366,7 +368,7 @@ func TestClient_DisconnectStreamManager(t *testing.T) {
|
|||
var client *Client
|
||||
var err error
|
||||
router := NewRouter()
|
||||
if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil {
|
||||
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
|
||||
t.Errorf("cannot create XMPP client: %s", err)
|
||||
}
|
||||
|
||||
|
@ -386,6 +388,162 @@ func TestClient_DisconnectStreamManager(t *testing.T) {
|
|||
mock.Stop()
|
||||
}
|
||||
|
||||
func Test_ClientPostConnectHook(t *testing.T) {
|
||||
done := make(chan struct{})
|
||||
// Handler for Mock server
|
||||
h := func(t *testing.T, sc *ServerConn) {
|
||||
handlerClientConnectSuccess(t, sc)
|
||||
done <- struct{}{}
|
||||
}
|
||||
|
||||
hookChan := make(chan struct{})
|
||||
mock := &ServerMock{}
|
||||
testServerAddress := fmt.Sprintf("%s:%d", testClientDomain, testClientPostConnectHook)
|
||||
|
||||
mock.Start(t, testServerAddress, h)
|
||||
config := Config{
|
||||
TransportConfiguration: TransportConfiguration{
|
||||
Address: testServerAddress,
|
||||
},
|
||||
Jid: "test@localhost",
|
||||
Credential: Password("test"),
|
||||
Insecure: true}
|
||||
|
||||
var client *Client
|
||||
var err error
|
||||
router := NewRouter()
|
||||
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
|
||||
t.Errorf("connect create XMPP client: %s", err)
|
||||
}
|
||||
|
||||
// The post connection client hook should just write to a channel that we will read later.
|
||||
client.PostFirstConnHook = func() error {
|
||||
go func() {
|
||||
hookChan <- struct{}{}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
// Handle a possible error
|
||||
errChan := make(chan error)
|
||||
errorHandler := func(err error) {
|
||||
errChan <- err
|
||||
}
|
||||
client.ErrorHandler = errorHandler
|
||||
if err = client.Connect(); err != nil {
|
||||
t.Errorf("XMPP connection failed: %s", err)
|
||||
}
|
||||
|
||||
// Check if the post connection client hook was correctly called
|
||||
select {
|
||||
case err := <-errChan: // If the server sends an error, or there is a connection error
|
||||
t.Fatal(err.Error())
|
||||
case <-time.After(defaultChannelTimeout): // If we timeout
|
||||
t.Fatal("Failed to call post connection client hook")
|
||||
case <-hookChan:
|
||||
// Test succeeded, channel was written to.
|
||||
}
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
mock.Stop()
|
||||
case <-time.After(defaultChannelTimeout):
|
||||
t.Fatal("The mock server failed to finish its job !")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ClientPostReconnectHook(t *testing.T) {
|
||||
hookChan := make(chan struct{})
|
||||
// Setup Mock server
|
||||
mock := ServerMock{}
|
||||
mock.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
|
||||
checkClientOpenStream(t, sc)
|
||||
|
||||
sendStreamFeatures(t, sc) // Send initial features
|
||||
readAuth(t, sc.decoder)
|
||||
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||
|
||||
checkClientOpenStream(t, sc) // Reset stream
|
||||
sendFeaturesStreamManagment(t, sc) // Send post auth features
|
||||
bind(t, sc)
|
||||
enableStreamManagement(t, sc, false, true)
|
||||
})
|
||||
|
||||
// Test / Check result
|
||||
config := Config{
|
||||
TransportConfiguration: TransportConfiguration{
|
||||
Address: testXMPPAddress,
|
||||
},
|
||||
Jid: "test@localhost",
|
||||
Credential: Password("test"),
|
||||
Insecure: true,
|
||||
StreamManagementEnable: true,
|
||||
streamManagementResume: true} // Enable stream management
|
||||
|
||||
var client *Client
|
||||
router := NewRouter()
|
||||
client, err := NewClient(&config, router, clientDefaultErrorHandler)
|
||||
if err != nil {
|
||||
t.Errorf("connect create XMPP client: %s", err)
|
||||
}
|
||||
|
||||
client.PostReconnectHook = func() error {
|
||||
go func() {
|
||||
hookChan <- struct{}{}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
err = client.Connect()
|
||||
if err != nil {
|
||||
t.Fatalf("could not connect client to mock server: %s", err)
|
||||
}
|
||||
|
||||
transp, ok := client.transport.(*XMPPTransport)
|
||||
if !ok {
|
||||
t.Fatalf("problem with client transport ")
|
||||
}
|
||||
|
||||
transp.conn.Close()
|
||||
mock.Stop()
|
||||
|
||||
// Check if the client can have its connection resumed using its state but also its configuration
|
||||
if !IsStreamResumable(client) {
|
||||
t.Fatalf("should support resumption")
|
||||
}
|
||||
|
||||
// Reboot server. We need to make a new one because (at least for now) the mock server can only have one handler
|
||||
// and they should be different between a first connection and a stream resume since exchanged messages
|
||||
// are different (See XEP-0198)
|
||||
mock2 := ServerMock{}
|
||||
mock2.Start(t, testXMPPAddress, func(t *testing.T, sc *ServerConn) {
|
||||
// Reconnect
|
||||
checkClientOpenStream(t, sc)
|
||||
|
||||
sendStreamFeatures(t, sc) // Send initial features
|
||||
readAuth(t, sc.decoder)
|
||||
sc.connection.Write([]byte("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"/>"))
|
||||
|
||||
checkClientOpenStream(t, sc) // Reset stream
|
||||
sendFeaturesStreamManagment(t, sc) // Send post auth features
|
||||
resumeStream(t, sc)
|
||||
})
|
||||
|
||||
// Reconnect
|
||||
err = client.Resume()
|
||||
if err != nil {
|
||||
t.Fatalf("could not connect client to mock server: %s", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-time.After(defaultChannelTimeout): // If we timeout
|
||||
t.Fatal("Failed to call post connection client hook")
|
||||
case <-hookChan:
|
||||
// Test succeeded, channel was written to.
|
||||
}
|
||||
|
||||
mock2.Stop()
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Basic XMPP Server Mock Handlers.
|
||||
|
||||
|
@ -449,7 +607,7 @@ func checkClientOpenStream(t *testing.T, sc *ServerConn) {
|
|||
var token xml.Token
|
||||
token, err := sc.decoder.Token()
|
||||
if err != nil {
|
||||
t.Errorf("cannot read next token: %s", err)
|
||||
t.Fatalf("cannot read next token: %s", err)
|
||||
}
|
||||
|
||||
switch elem := token.(type) {
|
||||
|
@ -464,6 +622,7 @@ func checkClientOpenStream(t *testing.T, sc *ServerConn) {
|
|||
}
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -472,7 +631,6 @@ func mockClientConnection(t *testing.T, serverHandler func(*testing.T, *ServerCo
|
|||
testServerAddress := fmt.Sprintf("%s:%d", testClientDomain, port)
|
||||
|
||||
mock.Start(t, testServerAddress, serverHandler)
|
||||
|
||||
config := Config{
|
||||
TransportConfiguration: TransportConfiguration{
|
||||
Address: testServerAddress,
|
||||
|
@ -484,7 +642,7 @@ func mockClientConnection(t *testing.T, serverHandler func(*testing.T, *ServerCo
|
|||
var client *Client
|
||||
var err error
|
||||
router := NewRouter()
|
||||
if client, err = NewClient(config, router, clientDefaultErrorHandler); err != nil {
|
||||
if client, err = NewClient(&config, router, clientDefaultErrorHandler); err != nil {
|
||||
t.Errorf("connect create XMPP client: %s", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ func sendxmpp(cmd *cobra.Command, args []string) {
|
|||
msgText := args[1]
|
||||
|
||||
var err error
|
||||
client, err := xmpp.NewClient(xmpp.Config{
|
||||
client, err := xmpp.NewClient(&xmpp.Config{
|
||||
TransportConfiguration: xmpp.TransportConfiguration{
|
||||
Address: viper.GetString("addr"),
|
||||
},
|
||||
|
|
|
@ -65,6 +65,7 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
|||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
|
@ -92,6 +93,7 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
|||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
|
@ -148,10 +150,12 @@ github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb6
|
|||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk=
|
||||
github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
|
@ -159,6 +163,7 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1
|
|||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A=
|
||||
|
@ -207,6 +212,7 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190927073244-c990c680b611 h1:q9u40nxWT5zRClI/uU9dHCiYGottAg6Nzz4YUQyHxdA=
|
||||
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
@ -231,6 +237,7 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33
|
|||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
|
@ -238,9 +245,11 @@ gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bl
|
|||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8=
|
||||
nhooyr.io/websocket v1.6.5 h1:8TzpkldRfefda5JST+CnOH135bzVPz5uzfn/AF+gVKg=
|
||||
nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY=
|
||||
|
|
|
@ -60,11 +60,10 @@ func NewComponent(opts ComponentOptions, r *Router, errorHandler func(error)) (*
|
|||
// Connect triggers component connection to XMPP server component port.
|
||||
// TODO: Failed handshake should be a permanent error
|
||||
func (c *Component) Connect() error {
|
||||
var state SMState
|
||||
return c.Resume(state)
|
||||
return c.Resume()
|
||||
}
|
||||
|
||||
func (c *Component) Resume(sm SMState) error {
|
||||
func (c *Component) Resume() error {
|
||||
var err error
|
||||
var streamId string
|
||||
if c.ComponentOptions.TransportConfiguration.Domain == "" {
|
||||
|
@ -73,16 +72,13 @@ func (c *Component) Resume(sm SMState) error {
|
|||
c.transport, err = NewComponentTransport(c.ComponentOptions.TransportConfiguration)
|
||||
if err != nil {
|
||||
c.updateState(StatePermanentError)
|
||||
|
||||
return NewConnError(err, true)
|
||||
}
|
||||
|
||||
if streamId, err = c.transport.Connect(); err != nil {
|
||||
c.updateState(StatePermanentError)
|
||||
|
||||
return NewConnError(err, true)
|
||||
}
|
||||
c.updateState(StateConnected)
|
||||
|
||||
// Authentication
|
||||
if err := c.sendWithWriter(c.transport, []byte(fmt.Sprintf("<handshake>%s</handshake>", c.handshake(streamId)))); err != nil {
|
||||
|
|
11
config.go
11
config.go
|
@ -21,4 +21,15 @@ type Config struct {
|
|||
// Insecure can be set to true to allow to open a session without TLS. If TLS
|
||||
// is supported on the server, we will still try to use it.
|
||||
Insecure bool
|
||||
|
||||
// Activate stream management process during session
|
||||
StreamManagementEnable bool
|
||||
// Enable stream management resume capability
|
||||
streamManagementResume bool
|
||||
}
|
||||
|
||||
// IsStreamResumable tells if a stream session is resumable by reading the "config" part of a client.
|
||||
// It checks if stream management is enabled, and if stream resumption was set and accepted by the server.
|
||||
func IsStreamResumable(c *Client) bool {
|
||||
return c.config.StreamManagementEnable && c.config.streamManagementResume
|
||||
}
|
||||
|
|