1package xmpp
2
3import (
4	"encoding/xml"
5	"io"
6	"io/ioutil"
7	"log"
8	"testing"
9	"time"
10
11	"github.com/coyim/coyim/i18n"
12	"github.com/coyim/coyim/xmpp/data"
13	"github.com/coyim/gotk3adapter/glib_mock"
14
15	. "gopkg.in/check.v1"
16)
17
18func Test(t *testing.T) { TestingT(t) }
19
20func init() {
21	log.SetOutput(ioutil.Discard)
22	i18n.InitLocalization(&glib_mock.Mock{})
23}
24
25type XMPPSuite struct{}
26
27var _ = Suite(&XMPPSuite{})
28
29func (s *XMPPSuite) TestDiscoReplyVerSimple(c *C) {
30	expect := "QgayPKawpkPSDYmwT/WM94uAlu0="
31	input := []byte(`
32  <query xmlns='http://jabber.org/protocol/disco#info'
33         node='http://code.google.com/p/exodus#QgayPKawpkPSDYmwT/WM94uAlu0='>
34    <identity category='client' name='Exodus 0.9.1' type='pc'/>
35    <feature var='http://jabber.org/protocol/caps'/>
36    <feature var='http://jabber.org/protocol/disco#info'/>
37    <feature var='http://jabber.org/protocol/disco#items'/>
38    <feature var='http://jabber.org/protocol/muc'/>
39  </query>
40  `)
41	var dr data.DiscoveryInfoQuery
42	c.Assert(xml.Unmarshal(input, &dr), IsNil)
43	hash, err := VerificationString(&dr)
44	c.Assert(err, IsNil)
45	c.Assert(hash, Equals, expect)
46}
47
48func (s *XMPPSuite) TestDiscoReplyVerComplex(c *C) {
49	expect := "q07IKJEyjvHSyhy//CH0CxmKi8w="
50	input := []byte(`
51  <query xmlns='http://jabber.org/protocol/disco#info'
52         node='http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w='>
53    <identity xml:lang='en' category='client' name='Psi 0.11' type='pc'/>
54    <identity xml:lang='el' category='client' name='Ψ 0.11' type='pc'/>
55    <feature var='http://jabber.org/protocol/caps'/>
56    <feature var='http://jabber.org/protocol/disco#info'/>
57    <feature var='http://jabber.org/protocol/disco#items'/>
58    <feature var='http://jabber.org/protocol/muc'/>
59    <x xmlns='jabber:x:data' type='result'>
60      <field var='FORM_TYPE' type='hidden'>
61        <value>urn:xmpp:dataforms:softwareinfo</value>
62      </field>
63      <field var='ip_version'>
64        <value>ipv4</value>
65        <value>ipv6</value>
66      </field>
67      <field var='os'>
68        <value>Mac</value>
69      </field>
70      <field var='os_version'>
71        <value>10.5.1</value>
72      </field>
73      <field var='software'>
74        <value>Psi</value>
75      </field>
76      <field var='software_version'>
77        <value>0.11</value>
78      </field>
79    </x>
80  </query>
81`)
82	var dr data.DiscoveryInfoQuery
83	c.Assert(xml.Unmarshal(input, &dr), IsNil)
84	hash, err := VerificationString(&dr)
85	c.Assert(err, IsNil)
86	c.Assert(hash, Equals, expect)
87}
88
89func (s *XMPPSuite) TestConnClose_sendsAStreamCloseTagWhenWeCloseFirst(c *C) {
90	mockIn := &mockConnIOReaderWriter{
91		read: []byte("<?xml version='1.0'?><str:stream xmlns:str='http://etherx.jabber.org/streams' version='1.0'></str:stream>"),
92	}
93	mockCloser := &mockConnIOReaderWriter{}
94
95	conn := newConn()
96	conn.in = xml.NewDecoder(mockIn)
97	conn.out = mockCloser
98	conn.rawOut = mockCloser
99
100	nextElement(conn.in) // Reads the opening tag and make the unmarshaller happy
101
102	done := make(chan bool)
103	go func() {
104		// This is sadly necessary, since the call to conn.Next() needs to happen AFTER the first few lines of conn.Close()
105		// has executed. Otherwise there will be a racecondition where conn.Close() sometimes will report that
106		// the connection has already been executed.
107		time.Sleep(time.Duration(2) * time.Second)
108
109		stanza, err := conn.Next() // Reads the closing tag
110
111		c.Assert(err, IsNil)
112		c.Assert(stanza, DeepEquals, data.Stanza{
113			Name:  xml.Name{Space: "http://etherx.jabber.org/streams", Local: "stream"},
114			Value: &data.StreamClose{},
115		})
116
117		done <- true
118	}()
119
120	// blocks until it receives the </stream> or timeouts
121	c.Assert(conn.Close(), IsNil)
122	c.Assert(mockCloser.CalledClose(), Equals, true)
123	c.Assert(mockCloser.Written(), DeepEquals, []byte("</stream:stream>"))
124
125	<-done
126}
127
128func (s *XMPPSuite) TestConnNext_replyWithAStreamCloseTagWhenTheyCloseFirst(c *C) {
129	mockIn := &mockConnIOReaderWriter{
130		read: []byte("<?xml version='1.0'?><str:stream xmlns:str='http://etherx.jabber.org/streams' version='1.0'></str:stream>"),
131	}
132	mockCloser := &mockConnIOReaderWriter{}
133
134	conn := newConn()
135	conn.in = xml.NewDecoder(mockIn)
136	conn.out = mockCloser
137	conn.rawOut = mockCloser
138
139	nextElement(conn.in)       // Reads the opening tag and make the unmarshaller happy
140	stanza, err := conn.Next() // Reads the closing tag
141
142	c.Assert(err, IsNil)
143	c.Assert(stanza, DeepEquals, data.Stanza{
144		Name:  xml.Name{Space: "http://etherx.jabber.org/streams", Local: "stream"},
145		Value: &data.StreamClose{},
146	})
147
148	c.Assert(mockCloser.CalledClose(), Equals, true)
149	c.Assert(mockCloser.write, DeepEquals, []byte("</stream:stream>"))
150
151	err = conn.Close()
152	c.Assert(err, NotNil)
153	c.Assert(err.Error(), Equals, "xmpp: the connection is already closed")
154}
155
156func (s *XMPPSuite) TestConnNextEOF(c *C) {
157	mockIn := &mockConnIOReaderWriter{err: io.EOF}
158	conn := conn{
159		in: xml.NewDecoder(mockIn),
160	}
161	stanza, err := conn.Next()
162	c.Assert(stanza.Name, Equals, xml.Name{})
163	c.Assert(stanza.Value, IsNil)
164	c.Assert(err, Equals, io.EOF)
165}
166
167func (s *XMPPSuite) TestConnNextErr(c *C) {
168	mockIn := &mockConnIOReaderWriter{
169		read: []byte(`
170      <field var='os'>
171        <value>Mac</value>
172      </field>
173		`),
174	}
175	conn := conn{
176		in: xml.NewDecoder(mockIn),
177	}
178	stanza, err := conn.Next()
179	c.Assert(stanza.Name, Equals, xml.Name{})
180	c.Assert(stanza.Value, IsNil)
181	c.Assert(err.Error(), Equals, "unexpected XMPP message  <field/>")
182}
183
184func (s *XMPPSuite) TestConnNextIQSet(c *C) {
185	mockIn := &mockConnIOReaderWriter{
186		read: []byte(`
187<iq to='example.com'
188    xmlns='jabber:client'
189    type='set'
190    id='sess_1'>
191  <session xmlns='urn:ietf:params:xml:ns:xmpp-session'/>
192</iq>
193  `),
194	}
195	conn := conn{
196		in: xml.NewDecoder(mockIn),
197	}
198	stanza, err := conn.Next()
199	c.Assert(stanza.Name, Equals, xml.Name{Space: NsClient, Local: "iq"})
200	iq, ok := stanza.Value.(*data.ClientIQ)
201	c.Assert(ok, Equals, true)
202	c.Assert(iq.To, Equals, "example.com")
203	c.Assert(iq.Type, Equals, "set")
204	c.Assert(err, IsNil)
205}
206
207func (s *XMPPSuite) TestConnNextIQResult(c *C) {
208	mockIn := &mockConnIOReaderWriter{
209		read: []byte(`
210<iq from='example.com'
211    xmlns='jabber:client'
212    type='result'
213    id='sess_1'/>
214  `),
215	}
216	conn := conn{
217		in: xml.NewDecoder(mockIn),
218	}
219	stanza, err := conn.Next()
220	c.Assert(stanza.Name, Equals, xml.Name{Space: NsClient, Local: "iq"})
221	iq, ok := stanza.Value.(*data.ClientIQ)
222	c.Assert(ok, Equals, true)
223	c.Assert(iq.From, Equals, "example.com")
224	c.Assert(iq.Type, Equals, "result")
225	c.Assert(err, ErrorMatches, "xmpp: failed to parse id from iq: .*")
226}
227
228func (s *XMPPSuite) TestConnCancelError(c *C) {
229	conn := conn{}
230	ok := conn.Cancel(conn.getCookie())
231	c.Assert(ok, Equals, false)
232}
233
234func (s *XMPPSuite) TestConnCancelOK(c *C) {
235	conn := conn{}
236	cookie := conn.getCookie()
237	ch := make(chan data.Stanza, 1)
238	conn.inflights = make(map[data.Cookie]inflight)
239	conn.inflights[cookie] = inflight{ch, ""}
240	ok := conn.Cancel(cookie)
241	c.Assert(ok, Equals, true)
242	_, ok = conn.inflights[cookie]
243	c.Assert(ok, Equals, false)
244}
245
246func (s *XMPPSuite) TestConnRequestRoster(c *C) {
247	mockOut := mockConnIOReaderWriter{}
248	conn := conn{
249		out: &mockOut,
250	}
251	conn.inflights = make(map[data.Cookie]inflight)
252	ch, cookie, err := conn.RequestRoster()
253	c.Assert(string(mockOut.write), Matches, "<iq type='get' id='.*'><query xmlns='jabber:iq:roster'/></iq>")
254	c.Assert(ch, NotNil)
255	c.Assert(cookie, NotNil)
256	c.Assert(err, IsNil)
257}
258
259func (s *XMPPSuite) TestConnRequestRosterErr(c *C) {
260	mockOut := mockConnIOReaderWriter{err: io.EOF}
261	conn := conn{
262		out: &mockOut,
263	}
264	conn.inflights = make(map[data.Cookie]inflight)
265	ch, cookie, err := conn.RequestRoster()
266	c.Assert(string(mockOut.write), Matches, "<iq type='get' id='.*'><query xmlns='jabber:iq:roster'/></iq>")
267	c.Assert(ch, IsNil)
268	c.Assert(cookie, NotNil)
269	c.Assert(err, Equals, io.EOF)
270}
271
272func (s *XMPPSuite) TestParseRoster(c *C) {
273	iq := data.ClientIQ{}
274	iq.Query = []byte(`
275  <query xmlns='jabber:iq:roster'>
276    <item jid='romeo@example.net'
277          name='Romeo'
278          subscription='both'>
279      <group>Friends</group>
280    </item>
281    <item jid='mercutio@example.org'
282          name='Mercutio'
283          subscription='from'>
284      <group>Friends</group>
285    </item>
286    <item jid='benvolio@example.org'
287          name='Benvolio'
288          subscription='both'>
289      <group>Friends</group>
290    </item>
291  </query>
292  `)
293	reply := data.Stanza{
294		Value: &iq,
295	}
296	rosterEntrys, err := data.ParseRoster(reply)
297	c.Assert(rosterEntrys, NotNil)
298	c.Assert(err, IsNil)
299}
300
301func (s *XMPPSuite) TestConnSend(c *C) {
302	mockOut := mockConnIOReaderWriter{}
303	conn := conn{
304		out: &mockOut,
305		jid: "jid",
306	}
307	err := conn.Send("example@xmpp.com", "message")
308	c.Assert(string(mockOut.write), Matches, "<message to='example@xmpp.com' from='jid' type='chat'><body>message</body><nos:x xmlns:nos='google:nosave' value='enabled'/><arc:record xmlns:arc='http://jabber.org/protocol/archive' otr='require'/><no-copy xmlns='urn:xmpp:hints'/><no-permanent-store xmlns='urn:xmpp:hints'/><private xmlns='urn:xmpp:carbons:2'/></message>")
309	c.Assert(err, IsNil)
310}
311