aeden-contacts 0.2.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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