aeden-contacts 0.2.15

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.rdoc +51 -0
  3. data/Rakefile +71 -0
  4. data/VERSION.yml +4 -0
  5. data/lib/config/contacts.yml +10 -0
  6. data/lib/contacts/flickr.rb +133 -0
  7. data/lib/contacts/google.rb +387 -0
  8. data/lib/contacts/google_oauth.rb +91 -0
  9. data/lib/contacts/version.rb +9 -0
  10. data/lib/contacts/windows_live.rb +164 -0
  11. data/lib/contacts/yahoo.rb +236 -0
  12. data/lib/contacts.rb +55 -0
  13. data/spec/contact_spec.rb +61 -0
  14. data/spec/feeds/contacts.yml +10 -0
  15. data/spec/feeds/flickr/auth.getFrob.xml +4 -0
  16. data/spec/feeds/flickr/auth.getToken.xml +5 -0
  17. data/spec/feeds/google-many.xml +48 -0
  18. data/spec/feeds/google-single.xml +46 -0
  19. data/spec/feeds/wl_contacts.xml +29 -0
  20. data/spec/feeds/yh_contacts.txt +119 -0
  21. data/spec/feeds/yh_credential.xml +28 -0
  22. data/spec/flickr/auth_spec.rb +80 -0
  23. data/spec/gmail/auth_spec.rb +70 -0
  24. data/spec/gmail/fetching_spec.rb +198 -0
  25. data/spec/rcov.opts +2 -0
  26. data/spec/spec.opts +2 -0
  27. data/spec/spec_helper.rb +84 -0
  28. data/spec/windows_live/windows_live_spec.rb +34 -0
  29. data/spec/yahoo/yahoo_spec.rb +83 -0
  30. data/vendor/fakeweb/CHANGELOG +80 -0
  31. data/vendor/fakeweb/LICENSE.txt +281 -0
  32. data/vendor/fakeweb/README.rdoc +160 -0
  33. data/vendor/fakeweb/Rakefile +57 -0
  34. data/vendor/fakeweb/fakeweb.gemspec +13 -0
  35. data/vendor/fakeweb/lib/fake_web/ext/net_http.rb +58 -0
  36. data/vendor/fakeweb/lib/fake_web/registry.rb +78 -0
  37. data/vendor/fakeweb/lib/fake_web/responder.rb +88 -0
  38. data/vendor/fakeweb/lib/fake_web/response.rb +10 -0
  39. data/vendor/fakeweb/lib/fake_web/socket_delegator.rb +24 -0
  40. data/vendor/fakeweb/lib/fake_web.rb +152 -0
  41. data/vendor/fakeweb/test/fixtures/test_example.txt +1 -0
  42. data/vendor/fakeweb/test/fixtures/test_request +21 -0
  43. data/vendor/fakeweb/test/test_allow_net_connect.rb +41 -0
  44. data/vendor/fakeweb/test/test_fake_web.rb +453 -0
  45. data/vendor/fakeweb/test/test_fake_web_open_uri.rb +62 -0
  46. data/vendor/fakeweb/test/test_helper.rb +52 -0
  47. data/vendor/fakeweb/test/test_query_string.rb +37 -0
  48. data/vendor/windowslivelogin.rb +1151 -0
  49. metadata +108 -0
@@ -0,0 +1,46 @@
1
+ <!-- source: http://code.google.com/apis/contacts/developers_guide_protocol.html -->
2
+ <feed xmlns='http://www.w3.org/2005/Atom'
3
+ xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/'
4
+ xmlns:gd='http://schemas.google.com/g/2005'>
5
+ <id>http://www.google.com/m8/feeds/contacts/liz%40gmail.com/base</id>
6
+ <updated>2008-03-05T12:36:38.836Z</updated>
7
+ <category scheme='http://schemas.google.com/g/2005#kind'
8
+ term='http://schemas.google.com/contact/2008#contact' />
9
+ <title type='text'>Contacts</title>
10
+ <link rel='http://schemas.google.com/g/2005#feed'
11
+ type='application/atom+xml'
12
+ href='http://www.google.com/m8/feeds/contacts/liz%40gmail.com/base' />
13
+ <link rel='http://schemas.google.com/g/2005#post'
14
+ type='application/atom+xml'
15
+ href='http://www.google.com/m8/feeds/contacts/liz%40gmail.com/base' />
16
+ <link rel='self' type='application/atom+xml'
17
+ href='http://www.google.com/m8/feeds/contacts/liz%40gmail.com/base?max-results=25' />
18
+ <author>
19
+ <name>Elizabeth Bennet</name>
20
+ <email>liz@gmail.com</email>
21
+ </author>
22
+ <generator version='1.0' uri='http://www.google.com/m8/feeds/contacts'>
23
+ Contacts
24
+ </generator>
25
+ <openSearch:totalResults>1</openSearch:totalResults>
26
+ <openSearch:startIndex>1</openSearch:startIndex>
27
+ <openSearch:itemsPerPage>25</openSearch:itemsPerPage>
28
+ <entry>
29
+ <id>
30
+ http://www.google.com/m8/feeds/contacts/liz%40gmail.com/base/c9012de
31
+ </id>
32
+ <updated>2008-03-05T12:36:38.835Z</updated>
33
+ <category scheme='http://schemas.google.com/g/2005#kind'
34
+ term='http://schemas.google.com/contact/2008#contact' />
35
+ <title type='text'>Fitzgerald</title>
36
+ <link rel='self' type='application/atom+xml'
37
+ href='http://www.google.com/m8/feeds/contacts/liz%40gmail.com/base/c9012de' />
38
+ <link rel='edit' type='application/atom+xml'
39
+ href='http://www.google.com/m8/feeds/contacts/liz%40gmail.com/base/c9012de/1204720598835000' />
40
+ <gd:phoneNumber rel='http://schemas.google.com/g/2005#home'
41
+ primary='true'>
42
+ 456
43
+ </gd:phoneNumber>
44
+ <gd:email label="Personal" rel="http://schemas.google.com/g/2005#home" address="fubar@gmail.com" primary="true" />
45
+ </entry>
46
+ </feed>
@@ -0,0 +1,29 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <LiveContacts>
3
+ <Owner>
4
+ <WindowsLiveID>hugo@hotmail.com</WindowsLiveID>
5
+ </Owner>
6
+ <Contacts>
7
+ <Contact>
8
+ <Profiles>
9
+ <Personal />
10
+ </Profiles>
11
+ <PreferredEmail>froz@gmail.com</PreferredEmail>
12
+ </Contact>
13
+ <Contact>
14
+ <Profiles>
15
+ <Personal>
16
+ <FirstName>Rafael</FirstName>
17
+ <LastName>Timbo</LastName>
18
+ </Personal>
19
+ </Profiles>
20
+ <PreferredEmail>timbo@hotmail.com</PreferredEmail>
21
+ </Contact>
22
+ <Contact>
23
+ <Profiles>
24
+ <Personal />
25
+ </Profiles>
26
+ <PreferredEmail>betinho@hotmail.com</PreferredEmail>
27
+ </Contact>
28
+ </Contacts>
29
+ </LiveContacts>
@@ -0,0 +1,119 @@
1
+ {
2
+ "type":"search_response",
3
+ "contacts":[
4
+ {
5
+ "type":"contact",
6
+ "fields":[
7
+ {
8
+ "type":"email",
9
+ "data":"hugo.barauna@gmail.com",
10
+ "fid":2,
11
+ "categories":[
12
+ ]
13
+ },
14
+ {
15
+ "type":"name",
16
+ "first":"Hugo",
17
+ "last":"Barauna",
18
+ "fid":1,
19
+ "categories":[
20
+ ]
21
+ }
22
+ ],
23
+ "cid":4,
24
+ "categories":[
25
+ ]
26
+ },
27
+ {
28
+ "type":"contact",
29
+ "fields":[
30
+ {
31
+ "type":"email",
32
+ "data":"nina@hotmail.com",
33
+ "fid":5,
34
+ "categories":[
35
+ ]
36
+ },
37
+ {
38
+ "type":"name",
39
+ "first":"Nina",
40
+ "last":"Benchimol",
41
+ "fid":4,
42
+ "categories":[
43
+ ]
44
+ }
45
+ ],
46
+ "cid":5,
47
+ "categories":[
48
+ ]
49
+ },
50
+ {
51
+ "type":"contact",
52
+ "fields":[
53
+ {
54
+ "type":"email",
55
+ "data":"and@yahoo.com",
56
+ "fid":7,
57
+ "categories":[
58
+ ]
59
+ },
60
+ {
61
+ "type":"name",
62
+ "first":"Andrea",
63
+ "last":"Dimitri",
64
+ "fid":6,
65
+ "categories":[
66
+ ]
67
+ }
68
+ ],
69
+ "cid":1,
70
+ "categories":[
71
+ ]
72
+ },
73
+ {
74
+ "type":"contact",
75
+ "fields":[
76
+ {
77
+ "type":"email",
78
+ "data":"ricardo@poli.usp.br",
79
+ "fid":11,
80
+ "categories":[
81
+ ]
82
+ },
83
+ {
84
+ "type":"name",
85
+ "first":"Ricardo",
86
+ "last":"Fiorelli",
87
+ "fid":10,
88
+ "categories":[
89
+ ]
90
+ }
91
+ ],
92
+ "cid":3,
93
+ "categories":[
94
+ ]
95
+ },
96
+ {
97
+ "type":"contact",
98
+ "fields":[
99
+ {
100
+ "type":"email",
101
+ "data":"pizinha@yahoo.com.br",
102
+ "fid":14,
103
+ "categories":[
104
+ ]
105
+ },
106
+ {
107
+ "type":"name",
108
+ "first":"Priscila",
109
+ "fid":13,
110
+ "categories":[
111
+ ]
112
+ }
113
+ ],
114
+ "cid":2,
115
+ "categories":[
116
+ ]
117
+ }
118
+ ]
119
+ }
@@ -0,0 +1,28 @@
1
+ <?xml version="1.0" encoding="utf-8" standalone="yes" ?>
2
+ <BBAuthTokenLoginResponse
3
+ xmlns:wsse='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd'
4
+ xmlns:yahooauth='urn:yahoo:auth'>
5
+ <Success>
6
+ <Cookie>
7
+ Y=cdunlEx76ZEeIdWyeJNOegxfy.jkeoULJCnc7Q0Vr8D5P.u.EE2vCa7G2MwBoULuZhvDZuJNqhHwF3v5RJ4dnsWsEDGOjYV1k6snoln3RlQmx0Ggxs0zAYgbaA4BFQk5ieAkpipq19l6GoD_k8IqXRfJN0Q54BbekC_O6Tj3zl2wV3YQK6Mi2MWBQFSBsO26Tw_1yMAF8saflF9EX1fQl4N.1yBr8UXb6LLDiPQmlISq1_c6S6rFbaOhSZMgO78f2iqZmUAk9RmCHrqPJiHEo.mJlxxHaQsuqTMf7rwLEHqK__Gi_bLypGtaslqeWyS0h2J.B5xwRC8snfEs3ct_kLXT3ngP_pK3MeMf2pe1TiJ4JXVciY9br.KJFUgNd4J6rmQsSFj4wPLoMGCETfVc.M8KLiaFHasZqXDyCE7tvd1khAjQ_xLfQKlg1GlBOWmbimQ1FhdHnsVj3svXjEGquRh8JI2sHIQrzoiqAPBf9WFKQcH0t_1dxf4MOH.7gJaYDPEozCW5EcCsYjuHup9xJKxyTddh5pk8yUg5bURzA.TwPalExMKsbv.RWFBhzWKuTp5guNcqjmUHcCoT19_qFENHX41Xf3texAnsDDGj
8
+ </Cookie>
9
+ <WSSID>tr.jZsW/ulc</WSSID>
10
+ <Timeout>3600</Timeout>
11
+ <YahooSOAPAuthHeader>
12
+ <wsse:Security>
13
+ <yahooauth:YahooAuthToken>
14
+ <yahooauth:Cookies>Y=cdunlEx76ZEeIdWyeJNOegxfy.jkeoULJCnc7Q0Vr8D5P.u.EE2vCa7G2MwBoULuZhvDZuJNqhHwF3v5RJ4dnsWsEDGOjYV1k6snoln3RlQmx0Ggxs0zAYgbaA4BFQk5ieAkpipq19l6GoD_k8IqXRfJN0Q54BbekC_O6Tj3zl2wV3YQK6Mi2MWBQFSBsO26Tw_1yMAF8saflF9EX1fQl4N.1yBr8UXb6LLDiPQmlISq1_c6S6rFbaOhSZMgO78f2iqZmUAk9RmCHrqPJiHEo.mJlxxHaQsuqTMf7rwLEHqK__Gi_bLypGtaslqeWyS0h2J.B5xwRC8snfEs3ct_kLXT3ngP_pK3MeMf2pe1TiJ4JXVciY9br.KJFUgNd4J6rmQsSFj4wPLoMGCETfVc.M8KLiaFHasZqXDyCE7tvd1khAjQ_xLfQKlg1GlBOWmbimQ1FhdHnsVj3svXjEGquRh8JI2sHIQrzoiqAPBf9WFKQcH0t_1dxf4MOH.7gJaYDPEozCW5EcCsYjuHup9xJKxyTddh5pk8yUg5bURzA.TwPalExMKsbv.RWFBhzWKuTp5guNcqjmUHcCoT19_qFENHX41Xf3texAnsDDGj
15
+ </yahooauth:Cookies>
16
+ <yahooauth:AppID>
17
+ jjmaQ37IkY0JH18hDRbVIbZ0r6BGYrbaQnm-
18
+ </yahooauth:AppID>
19
+ <yahooauth:WSSID>
20
+ tr.jZsW/ulc
21
+ </yahooauth:WSSID>
22
+ </yahooauth:YahooAuthToken>
23
+ </wsse:Security>
24
+ </YahooSOAPAuthHeader>
25
+ </Success>
26
+ </BBAuthTokenLoginResponse>
27
+ <!-- apil02.member.re3.yahoo.com uncompressed/chunked Mon Aug 11 19:00:37 PDT 2008 -->
28
+
@@ -0,0 +1,80 @@
1
+ require 'spec_helper'
2
+ require 'contacts/flickr'
3
+ require 'uri'
4
+
5
+ describe Contacts::Flickr, "authentication" do
6
+
7
+ before(:each) do
8
+ @f = Contacts::Flickr
9
+ end
10
+
11
+ describe "authentication for desktop apps" do
12
+ # Key, secret and signature come from the Flickr API docs.
13
+ # http://www.flickr.com/services/api/auth.howto.desktop.html
14
+ it "should generate a correct url to retrieve a frob" do
15
+ path, query = Contacts::Flickr.frob_url('9a0554259914a86fb9e7eb014e4e5d52', '000005fab4534d05').split('?')
16
+ path.should == '/services/rest/'
17
+
18
+ hsh = hash_from_query(query)
19
+ hsh[:method].should == 'flickr.auth.getFrob'
20
+ hsh[:api_key].should == '9a0554259914a86fb9e7eb014e4e5d52'
21
+ hsh[:api_sig].should == '8ad70cd3888ce493c8dde4931f7d6bd0'
22
+ hsh[:secret].should be_nil
23
+ end
24
+
25
+ it "should generate an authentication url from a response with a frob" do
26
+ response = mock_response
27
+ response.stubs(:body).returns sample_xml('flickr/auth.getFrob')
28
+ Contacts::Flickr.frob_from_response(response).should == '934-746563215463214621'
29
+ end
30
+
31
+ # The :api_sig parameter is documented wronly in the Flickr API docs. It says the string
32
+ # to sign ends in 'permswread', but this should be 'permsread' of course. Therefore
33
+ # the :api_sig here does not correspond to the Flickr docs, but it makes the spec valid.
34
+ it "should return an url to authenticate to containing a frob" do
35
+ response = mock_response
36
+ response.stubs(:body).returns sample_xml('flickr/auth.getFrob')
37
+ @f.expects(:http_start).returns(response)
38
+ uri = URI.parse @f.authentication_url('9a0554259914a86fb9e7eb014e4e5d52', '000005fab4534d05')
39
+
40
+ uri.host.should == 'www.flickr.com'
41
+ uri.scheme.should == 'http'
42
+ uri.path.should == '/services/auth/'
43
+ hsh = hash_from_query(uri.query)
44
+ hsh[:api_key].should == '9a0554259914a86fb9e7eb014e4e5d52'
45
+ hsh[:api_sig].should == '0d08a9522d152d2e43daaa2a932edf67'
46
+ hsh[:frob].should == '934-746563215463214621'
47
+ hsh[:perms].should == 'read'
48
+ hsh[:secret].should be_nil
49
+ end
50
+
51
+ it "should get a token from a frob" do
52
+ response = mock_response
53
+ response.stubs(:body).returns sample_xml('flickr/auth.getToken')
54
+ connection = mock('Connection')
55
+ connection.expects(:get).with do |value|
56
+ path, query = value.split('?')
57
+ path.should == '/services/rest/'
58
+
59
+ hsh = hash_from_query(query)
60
+ hsh[:method].should == 'flickr.auth.getToken'
61
+ hsh[:api_key].should == '9a0554259914a86fb9e7eb014e4e5d52'
62
+ hsh[:api_sig].should == 'a5902059792a7976d03be67bdb1e98fd'
63
+ hsh[:frob].should == '934-746563215463214621'
64
+ hsh[:secret].should be_nil
65
+ true
66
+ end
67
+ @f.expects(:http_start).returns(response).yields(connection)
68
+ @f.get_token_from_frob('9a0554259914a86fb9e7eb014e4e5d52', '000005fab4534d05', '934-746563215463214621').should == '45-76598454353455'
69
+ end
70
+
71
+ end
72
+
73
+ def hash_from_query(str)
74
+ str.split('&').inject({}) do |hsh, pair|
75
+ key, value = pair.split('=')
76
+ hsh[key.to_sym] = value
77
+ hsh
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,70 @@
1
+ require 'spec_helper'
2
+ require 'contacts/google'
3
+ require 'uri'
4
+
5
+ describe Contacts::Google, '.authentication_url' do
6
+
7
+ after :each do
8
+ FakeWeb.clean_registry
9
+ end
10
+
11
+ it 'generates a URL for target with default parameters' do
12
+ uri = parse_authentication_url('http://example.com/invite')
13
+
14
+ uri.host.should == 'www.google.com'
15
+ uri.scheme.should == 'https'
16
+ uri.query.split('&').sort.should == [
17
+ 'next=http%3A%2F%2Fexample.com%2Finvite',
18
+ 'scope=http%3A%2F%2Fwww.google.com%2Fm8%2Ffeeds%2Fcontacts%2F',
19
+ 'secure=0',
20
+ 'session=0'
21
+ ]
22
+ end
23
+
24
+ it 'should handle boolean parameters' do
25
+ pairs = parse_authentication_url(nil, :secure => true, :session => true).query.split('&')
26
+
27
+ pairs.should include('secure=1')
28
+ pairs.should include('session=1')
29
+ end
30
+
31
+ it 'skips parameters that have nil value' do
32
+ query = parse_authentication_url(nil, :secure => nil).query
33
+ query.should_not include('next')
34
+ query.should_not include('secure')
35
+ end
36
+
37
+ it 'should be able to exchange one-time for session token' do
38
+ FakeWeb::register_uri(:get, 'https://www.google.com/accounts/AuthSubSessionToken',
39
+ :string => "Token=G25aZ-v_8B\nExpiration=20061004T123456Z",
40
+ :verify => lambda { |req|
41
+ req['Authorization'].should == %(AuthSub token="dummytoken")
42
+ }
43
+ )
44
+
45
+ Contacts::Google.session_token('dummytoken').should == 'G25aZ-v_8B'
46
+ end
47
+
48
+ it "should support client login" do
49
+ FakeWeb::register_uri(:post, 'https://www.google.com/accounts/ClientLogin',
50
+ :method => 'POST',
51
+ :query => {
52
+ 'accountType' => 'GOOGLE', 'service' => 'cp', 'source' => 'Contacts-Ruby',
53
+ 'Email' => 'mislav@example.com', 'Passwd' => 'dummyPassword'
54
+ },
55
+ :string => "SID=klw4pHhL_ry4jl6\nLSID=Ij6k-7Ypnc1sxm\nAuth=EuoqMSjN5uo-3B"
56
+ )
57
+
58
+ Contacts::Google.client_login('mislav@example.com', 'dummyPassword').should == 'EuoqMSjN5uo-3B'
59
+ end
60
+
61
+ it "should support token authentication after client login" do
62
+ @gmail = Contacts::Google.new('dummytoken', 'default', true)
63
+ @gmail.headers['Authorization'].should == 'GoogleLogin auth="dummytoken"'
64
+ end
65
+
66
+ def parse_authentication_url(*args)
67
+ URI.parse Contacts::Google.authentication_url(*args)
68
+ end
69
+
70
+ end
@@ -0,0 +1,198 @@
1
+ require 'spec_helper'
2
+ require 'contacts/google'
3
+
4
+ describe Contacts::Google do
5
+
6
+ before :each do
7
+ @gmail = create
8
+ end
9
+
10
+ def create
11
+ Contacts::Google.new('dummytoken')
12
+ end
13
+
14
+ after :each do
15
+ FakeWeb.clean_registry
16
+ end
17
+
18
+ describe 'fetches contacts feed via HTTP GET' do
19
+ it 'with defaults' do
20
+ FakeWeb::register_uri(:get, 'www.google.com/m8/feeds/contacts/default/thin',
21
+ :string => 'thin results',
22
+ :verify => lambda { |req|
23
+ req['Authorization'].should == %(AuthSub token="dummytoken")
24
+ req['Accept-Encoding'].should == 'gzip'
25
+ req['User-Agent'].should == "Ruby Contacts v#{Contacts::VERSION::STRING} (gzip)"
26
+ }
27
+ )
28
+
29
+ response = @gmail.get({})
30
+ response.body.should == 'thin results'
31
+ end
32
+
33
+ it 'with explicit user ID and full projection' do
34
+ @gmail = Contacts::Google.new('dummytoken', 'person@example.com')
35
+ @gmail.projection = 'full'
36
+
37
+ FakeWeb::register_uri(:get, 'www.google.com/m8/feeds/contacts/person%40example.com/full',
38
+ :string => 'full results'
39
+ )
40
+
41
+ response = @gmail.get({})
42
+ response.body.should == 'full results'
43
+ end
44
+ end
45
+
46
+ it 'handles a normal response body' do
47
+ response = mock('HTTP response')
48
+ @gmail.expects(:get).returns(response)
49
+
50
+ response.expects(:'[]').with('Content-Encoding').returns(nil)
51
+ response.expects(:body).returns('<feed/>')
52
+
53
+ @gmail.expects(:parse_contacts).with('<feed/>')
54
+ @gmail.contacts
55
+ end
56
+
57
+ it 'handles gzipped response' do
58
+ response = mock('HTTP response')
59
+ @gmail.expects(:get).returns(response)
60
+
61
+ gzipped = StringIO.new
62
+ gzwriter = Zlib::GzipWriter.new gzipped
63
+ gzwriter.write(('a'..'z').to_a.join)
64
+ gzwriter.close
65
+
66
+ response.expects(:'[]').with('Content-Encoding').returns('gzip')
67
+ response.expects(:body).returns gzipped.string
68
+
69
+ @gmail.expects(:parse_contacts).with('abcdefghijklmnopqrstuvwxyz')
70
+ @gmail.contacts
71
+ end
72
+
73
+ it 'raises a fetching error when something goes awry' do
74
+ FakeWeb::register_uri(:get, 'www.google.com/m8/feeds/contacts/default/thin',
75
+ :status => [404, 'YOU FAIL']
76
+ )
77
+
78
+ lambda {
79
+ @gmail.get({})
80
+ }.should raise_error(Net::HTTPServerException)
81
+ end
82
+
83
+ it 'parses the resulting feed into name/email pairs' do
84
+ @gmail.stubs(:get)
85
+ @gmail.expects(:response_body).returns(sample_xml('google-single'))
86
+
87
+ found = @gmail.contacts
88
+ found.size.should == 1
89
+ contact = found.first
90
+ contact.name.should == 'Fitzgerald'
91
+ contact.emails.should == [{'primary' => 'true', 'type' => 'home', 'value' => 'fubar@gmail.com'}]
92
+ end
93
+
94
+ it 'parses a complex feed into name/email pairs' do
95
+ @gmail.stubs(:get)
96
+ @gmail.expects(:response_body).returns(sample_xml('google-many'))
97
+
98
+ found = @gmail.contacts
99
+ found.size.should == 4
100
+ found[0].name.should == 'Elizabeth Bennet'
101
+ found[0].emails.should == [{'primary' => 'true', 'type' => 'work', 'value' => 'liz@gmail.com'}, {'primary' => 'false', 'type' => 'home', 'value' => 'liz@example.org'}]
102
+ found[1].name.should == 'Poor Jack'
103
+ found[1].emails.should == []
104
+ found[2].name.should == 'William Paginate'
105
+ found[2].emails.should == [{'primary' => 'false', 'type' => 'other', 'value' => 'will_paginate@googlegroups.com'}]
106
+ found[3].name.should be_nil
107
+ found[3].emails.should == [{'primary' => 'false', 'type' => 'other', 'value' => 'anonymous@example.com'}]
108
+ end
109
+
110
+ it 'makes modification time available after parsing' do
111
+ @gmail.updated_at.should be_nil
112
+ @gmail.stubs(:get)
113
+ @gmail.expects(:response_body).returns(sample_xml('google-single'))
114
+
115
+ @gmail.contacts
116
+ u = @gmail.updated_at
117
+ u.year.should == 2008
118
+ u.day.should == 5
119
+ @gmail.updated_at_string.should == '2008-03-05T12:36:38.836Z'
120
+ end
121
+
122
+ describe 'GET query parameter handling' do
123
+
124
+ before :each do
125
+ @gmail = create
126
+ @gmail.stubs(:response_body)
127
+ @gmail.stubs(:parse_contacts)
128
+ end
129
+
130
+ it 'abstracts ugly parameters behind nicer ones' do
131
+ expect_params 'max-results' => '25',
132
+ 'orderby' => 'lastmodified',
133
+ 'sortorder' => 'ascending',
134
+ 'start-index' => '11',
135
+ 'updated-min' => 'datetime'
136
+
137
+ @gmail.contacts :limit => 25,
138
+ :offset => 10,
139
+ :order => 'lastmodified',
140
+ :descending => false,
141
+ :updated_after => 'datetime'
142
+ end
143
+
144
+ it 'should have implicit :descending with :order' do
145
+ expect_params 'orderby' => 'lastmodified',
146
+ 'sortorder' => 'descending',
147
+ 'max-results' => '200'
148
+
149
+ @gmail.contacts :order => 'lastmodified'
150
+ end
151
+
152
+ it 'should have default :limit of 200' do
153
+ expect_params 'max-results' => '200'
154
+ @gmail.contacts
155
+ end
156
+
157
+ it 'should skip nil values in parameters' do
158
+ expect_params 'start-index' => '1'
159
+ @gmail.contacts :limit => nil, :offset => 0
160
+ end
161
+
162
+ def expect_params(params)
163
+ query_string = Contacts::Google.query_string(params)
164
+ FakeWeb::register_uri(:get, "www.google.com/m8/feeds/contacts/default/thin?#{query_string}")
165
+ end
166
+
167
+ end
168
+
169
+ describe 'Retrieving all contacts (in chunks)' do
170
+
171
+ before :each do
172
+ @gmail = create
173
+ end
174
+
175
+ it 'should make only one API call when no more is needed' do
176
+ @gmail.expects(:contacts).with(instance_of(Hash)).once.returns((0..8).to_a)
177
+
178
+ @gmail.all_contacts({}, 10).should == (0..8).to_a
179
+ end
180
+
181
+ it 'should make multiple calls to :contacts when needed' do
182
+ @gmail.expects(:contacts).with(has_entries(:offset => 0 , :limit => 10)).returns(( 0..9 ).to_a)
183
+ @gmail.expects(:contacts).with(has_entries(:offset => 10, :limit => 10)).returns((10..19).to_a)
184
+ @gmail.expects(:contacts).with(has_entries(:offset => 20, :limit => 10)).returns((20..24).to_a)
185
+
186
+ @gmail.all_contacts({}, 10).should == (0..24).to_a
187
+ end
188
+
189
+ it 'should make one extra API call when not sure whether there are more contacts' do
190
+ @gmail.expects(:contacts).with(has_entries(:offset => 0 , :limit => 10)).returns((0..9).to_a)
191
+ @gmail.expects(:contacts).with(has_entries(:offset => 10, :limit => 10)).returns([])
192
+
193
+ @gmail.all_contacts({}, 10).should == (0..9).to_a
194
+ end
195
+
196
+ end
197
+
198
+ end
data/spec/rcov.opts ADDED
@@ -0,0 +1,2 @@
1
+ --exclude ^\/,^spec\/
2
+ --no-validator-links
data/spec/spec.opts ADDED
@@ -0,0 +1,2 @@
1
+ --colour
2
+ --reverse
@@ -0,0 +1,84 @@
1
+ require 'rubygems'
2
+ gem 'rspec', '~> 1.1.3'
3
+ require 'spec'
4
+ gem 'mocha', '~> 0.9.0'
5
+ require 'mocha'
6
+
7
+ require 'cgi'
8
+ require 'fake_web'
9
+ FakeWeb.allow_net_connect = false
10
+
11
+ module SampleFeeds
12
+ FEED_DIR = File.dirname(__FILE__) + '/feeds/'
13
+
14
+ def sample_xml(name)
15
+ File.read "#{FEED_DIR}#{name}.xml"
16
+ end
17
+ end
18
+
19
+ module HttpMocks
20
+ def mock_response(type = :success)
21
+ klass = case type
22
+ when :success then Net::HTTPSuccess
23
+ when :redirect then Net::HTTPRedirection
24
+ when :fail then Net::HTTPClientError
25
+ else type
26
+ end
27
+
28
+ klass.new(nil, nil, nil)
29
+ end
30
+
31
+ def mock_connection(ssl = true)
32
+ connection = mock('HTTP connection')
33
+ connection.stubs(:start)
34
+ connection.stubs(:finish)
35
+ if ssl
36
+ connection.expects(:use_ssl=).with(true)
37
+ connection.expects(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE)
38
+ end
39
+ connection
40
+ end
41
+ end
42
+
43
+ Spec::Runner.configure do |config|
44
+ config.include SampleFeeds, HttpMocks
45
+ # config.predicate_matchers[:swim] = :can_swim?
46
+
47
+ config.mock_with :mocha
48
+ end
49
+
50
+ module Mocha
51
+ module ParameterMatchers
52
+ def query_string(entries, partial = false)
53
+ QueryStringMatcher.new(entries, partial)
54
+ end
55
+ end
56
+ end
57
+
58
+ class QueryStringMatcher < Mocha::ParameterMatchers::Base
59
+
60
+ def initialize(entries, partial)
61
+ @entries = entries
62
+ @partial = partial
63
+ end
64
+
65
+ def matches?(available_parameters)
66
+ string = available_parameters.shift.split('?').last
67
+ broken = string.split('&').map { |pair| pair.split('=').map { |value| CGI.unescape(value) } }
68
+ hash = Hash[*broken.flatten]
69
+
70
+ if @partial
71
+ has_entry_matchers = @entries.map do |key, value|
72
+ Mocha::ParameterMatchers::HasEntry.new(key, value)
73
+ end
74
+ Mocha::ParameterMatchers::AllOf.new(*has_entry_matchers).matches?([hash])
75
+ else
76
+ @entries == hash
77
+ end
78
+ end
79
+
80
+ def mocha_inspect
81
+ "query_string(#{@entries.mocha_inspect})"
82
+ end
83
+
84
+ end