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.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +51 -0
- data/Rakefile +71 -0
- data/VERSION.yml +4 -0
- data/lib/config/contacts.yml +10 -0
- data/lib/contacts/flickr.rb +133 -0
- data/lib/contacts/google.rb +387 -0
- data/lib/contacts/google_oauth.rb +91 -0
- data/lib/contacts/version.rb +9 -0
- data/lib/contacts/windows_live.rb +164 -0
- data/lib/contacts/yahoo.rb +236 -0
- data/lib/contacts.rb +55 -0
- data/spec/contact_spec.rb +61 -0
- data/spec/feeds/contacts.yml +10 -0
- data/spec/feeds/flickr/auth.getFrob.xml +4 -0
- data/spec/feeds/flickr/auth.getToken.xml +5 -0
- data/spec/feeds/google-many.xml +48 -0
- data/spec/feeds/google-single.xml +46 -0
- data/spec/feeds/wl_contacts.xml +29 -0
- data/spec/feeds/yh_contacts.txt +119 -0
- data/spec/feeds/yh_credential.xml +28 -0
- data/spec/flickr/auth_spec.rb +80 -0
- data/spec/gmail/auth_spec.rb +70 -0
- data/spec/gmail/fetching_spec.rb +198 -0
- data/spec/rcov.opts +2 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +84 -0
- data/spec/windows_live/windows_live_spec.rb +34 -0
- data/spec/yahoo/yahoo_spec.rb +83 -0
- data/vendor/fakeweb/CHANGELOG +80 -0
- data/vendor/fakeweb/LICENSE.txt +281 -0
- data/vendor/fakeweb/README.rdoc +160 -0
- data/vendor/fakeweb/Rakefile +57 -0
- data/vendor/fakeweb/fakeweb.gemspec +13 -0
- data/vendor/fakeweb/lib/fake_web/ext/net_http.rb +58 -0
- data/vendor/fakeweb/lib/fake_web/registry.rb +78 -0
- data/vendor/fakeweb/lib/fake_web/responder.rb +88 -0
- data/vendor/fakeweb/lib/fake_web/response.rb +10 -0
- data/vendor/fakeweb/lib/fake_web/socket_delegator.rb +24 -0
- data/vendor/fakeweb/lib/fake_web.rb +152 -0
- data/vendor/fakeweb/test/fixtures/test_example.txt +1 -0
- data/vendor/fakeweb/test/fixtures/test_request +21 -0
- data/vendor/fakeweb/test/test_allow_net_connect.rb +41 -0
- data/vendor/fakeweb/test/test_fake_web.rb +453 -0
- data/vendor/fakeweb/test/test_fake_web_open_uri.rb +62 -0
- data/vendor/fakeweb/test/test_helper.rb +52 -0
- data/vendor/fakeweb/test/test_query_string.rb +37 -0
- data/vendor/windowslivelogin.rb +1151 -0
- 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
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -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
|