aurelian-contacts 0.3.1
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 +29 -0
- data/Rakefile +71 -0
- data/VERSION.yml +4 -0
- data/lib/contacts.rb +59 -0
- data/lib/contacts/flickr.rb +40 -0
- data/lib/contacts/google.rb +427 -0
- data/lib/contacts/google_oauth.rb +91 -0
- data/lib/contacts/version.rb +9 -0
- data/lib/contacts/windows_live.rb +186 -0
- data/lib/contacts/yahoo.rb +331 -0
- data/spec/contact_spec.rb +69 -0
- data/spec/gmail/auth_spec.rb +68 -0
- data/spec/gmail/fetching_spec.rb +197 -0
- data/spec/spec_helper.rb +91 -0
- data/spec/windows_live/windows_live_spec.rb +41 -0
- data/spec/yahoo/yahoo_spec.rb +95 -0
- data/vendor/windowslivelogin.rb +1151 -0
- metadata +72 -0
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'contacts'
|
3
|
+
|
4
|
+
describe Contacts::Contact do
|
5
|
+
describe 'instance' do
|
6
|
+
before do
|
7
|
+
@contact = Contacts::Contact.new('max@example.com', 'Max Power', 'maxpower', "Max", "Power")
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should have email" do
|
11
|
+
@contact.email.should == 'max@example.com'
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should have name" do
|
15
|
+
@contact.name.should == 'Max Power'
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should support a service id" do
|
19
|
+
@contact.service_id = 'service identifier'
|
20
|
+
@contact.service_id.should == 'service identifier'
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should support multiple emails" do
|
24
|
+
@contact.emails << 'maxpower@example.com'
|
25
|
+
@contact.email.should == 'max@example.com'
|
26
|
+
@contact.emails.should == ['max@example.com', 'maxpower@example.com']
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should support multiple ims" do
|
30
|
+
@contact.ims << {'value' => 'max', 'type' => 'skype'}
|
31
|
+
@contact.ims.should == [{'value' => 'max', 'type' => 'skype'}]
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should support multiple phones" do
|
35
|
+
@contact.phones << {'value' => '111 111 1111', 'type' => 'home'}
|
36
|
+
@contact.phones.should == [{'value' => '111 111 1111', 'type' => 'home'}]
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should support multiple addresses" do
|
40
|
+
@contact.addresses << {'formatted' => '111 SW 1st Street, New York, NY'}
|
41
|
+
@contact.addresses.should == [{'formatted' => '111 SW 1st Street, New York, NY'}]
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should have username" do
|
45
|
+
@contact.username.should == 'maxpower'
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should have firstname" do
|
49
|
+
@contact.firstname.should == 'Max'
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should have lastname" do
|
53
|
+
@contact.lastname.should == 'Power'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe '#inspect' do
|
58
|
+
it "should be nice" do
|
59
|
+
@contact = Contacts::Contact.new('max@example.com', 'Max Power', 'maxpower')
|
60
|
+
@contact.inspect.should == '#<Contacts::Contact "Max Power" (max@example.com)>'
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should be nice without email" do
|
64
|
+
@contact = Contacts::Contact.new(nil, 'Max Power', 'maxpower')
|
65
|
+
@contact.inspect.should == '#<Contacts::Contact "Max Power">'
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'contacts/google'
|
2
|
+
|
3
|
+
describe Contacts::Google, '.authentication_url' do
|
4
|
+
|
5
|
+
after :each do
|
6
|
+
FakeWeb.clean_registry
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'generates a URL for target with default parameters' do
|
10
|
+
uri = parse_authentication_url('http://example.com/invite')
|
11
|
+
|
12
|
+
uri.host.should == 'www.google.com'
|
13
|
+
uri.scheme.should == 'https'
|
14
|
+
uri.query.split('&').sort.should == [
|
15
|
+
'next=http%3A%2F%2Fexample.com%2Finvite',
|
16
|
+
'scope=http%3A%2F%2Fwww.google.com%2Fm8%2Ffeeds%2Fcontacts%2F',
|
17
|
+
'secure=0',
|
18
|
+
'session=0'
|
19
|
+
]
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should handle boolean parameters' do
|
23
|
+
pairs = parse_authentication_url(nil, :secure => true, :session => true).query.split('&')
|
24
|
+
|
25
|
+
pairs.should include('secure=1')
|
26
|
+
pairs.should include('session=1')
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'skips parameters that have nil value' do
|
30
|
+
query = parse_authentication_url(nil, :secure => nil).query
|
31
|
+
query.should_not include('next')
|
32
|
+
query.should_not include('secure')
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should be able to exchange one-time for session token' do
|
36
|
+
FakeWeb::register_uri(:get, 'https://www.google.com/accounts/AuthSubSessionToken',
|
37
|
+
:body => "Token=G25aZ-v_8B\nExpiration=20061004T123456Z",
|
38
|
+
:verify => lambda { |req|
|
39
|
+
req['Authorization'].should == %(AuthSub token="dummytoken")
|
40
|
+
}
|
41
|
+
)
|
42
|
+
|
43
|
+
Contacts::Google.session_token('dummytoken').should == 'G25aZ-v_8B'
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should support client login" do
|
47
|
+
FakeWeb::register_uri(:post, 'https://www.google.com/accounts/ClientLogin',
|
48
|
+
:method => 'POST',
|
49
|
+
:query => {
|
50
|
+
'accountType' => 'GOOGLE', 'service' => 'cp', 'source' => 'Contacts-Ruby',
|
51
|
+
'Email' => 'mislav@example.com', 'Passwd' => 'dummyPassword'
|
52
|
+
},
|
53
|
+
:body => "SID=klw4pHhL_ry4jl6\nLSID=Ij6k-7Ypnc1sxm\nAuth=EuoqMSjN5uo-3B"
|
54
|
+
)
|
55
|
+
|
56
|
+
Contacts::Google.client_login('mislav@example.com', 'dummyPassword').should == 'EuoqMSjN5uo-3B'
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should support token authentication after client login" do
|
60
|
+
@gmail = Contacts::Google.new('dummytoken', 'default', true)
|
61
|
+
@gmail.headers['Authorization'].should == 'GoogleLogin auth="dummytoken"'
|
62
|
+
end
|
63
|
+
|
64
|
+
def parse_authentication_url(*args)
|
65
|
+
URI.parse Contacts::Google.authentication_url(*args)
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
require 'contacts/google'
|
2
|
+
|
3
|
+
describe Contacts::Google do
|
4
|
+
|
5
|
+
before :each do
|
6
|
+
@gmail = create
|
7
|
+
end
|
8
|
+
|
9
|
+
def create
|
10
|
+
Contacts::Google.new('dummytoken')
|
11
|
+
end
|
12
|
+
|
13
|
+
after :each do
|
14
|
+
FakeWeb.clean_registry
|
15
|
+
end
|
16
|
+
|
17
|
+
describe 'fetches contacts feed via HTTP GET' do
|
18
|
+
it 'with defaults' do
|
19
|
+
FakeWeb::register_uri(:get, 'www.google.com/m8/feeds/contacts/default/thin',
|
20
|
+
:body => 'thin results',
|
21
|
+
:verify => lambda { |req|
|
22
|
+
req['Authorization'].should == %(AuthSub token="dummytoken")
|
23
|
+
req['Accept-Encoding'].should == 'gzip'
|
24
|
+
req['User-Agent'].should == "Ruby Contacts v#{Contacts::VERSION::STRING} (gzip)"
|
25
|
+
}
|
26
|
+
)
|
27
|
+
|
28
|
+
response = @gmail.get({})
|
29
|
+
response.body.should == 'thin results'
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'with explicit user ID and full projection' do
|
33
|
+
@gmail = Contacts::Google.new('dummytoken', 'person@example.com')
|
34
|
+
@gmail.projection = 'full'
|
35
|
+
|
36
|
+
FakeWeb::register_uri(:get, 'www.google.com/m8/feeds/contacts/person%40example.com/full',
|
37
|
+
:body => 'full results'
|
38
|
+
)
|
39
|
+
|
40
|
+
response = @gmail.get({})
|
41
|
+
response.body.should == 'full results'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'handles a normal response body' do
|
46
|
+
response = mock('HTTP response')
|
47
|
+
@gmail.expects(:get).returns(response)
|
48
|
+
|
49
|
+
response.expects(:'[]').with('Content-Encoding').returns(nil)
|
50
|
+
response.expects(:body).returns('<feed/>')
|
51
|
+
|
52
|
+
@gmail.expects(:parse_contacts).with('<feed/>')
|
53
|
+
@gmail.contacts
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'handles gzipped response' do
|
57
|
+
response = mock('HTTP response')
|
58
|
+
@gmail.expects(:get).returns(response)
|
59
|
+
|
60
|
+
gzipped = StringIO.new
|
61
|
+
gzwriter = Zlib::GzipWriter.new gzipped
|
62
|
+
gzwriter.write(('a'..'z').to_a.join)
|
63
|
+
gzwriter.close
|
64
|
+
|
65
|
+
response.expects(:'[]').with('Content-Encoding').returns('gzip')
|
66
|
+
response.expects(:body).returns gzipped.string
|
67
|
+
|
68
|
+
@gmail.expects(:parse_contacts).with('abcdefghijklmnopqrstuvwxyz')
|
69
|
+
@gmail.contacts
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'raises a fetching error when something goes awry' do
|
73
|
+
FakeWeb::register_uri(:get, 'www.google.com/m8/feeds/contacts/default/thin',
|
74
|
+
:status => [404, 'YOU FAIL']
|
75
|
+
)
|
76
|
+
|
77
|
+
lambda {
|
78
|
+
@gmail.get({})
|
79
|
+
}.should raise_error(Net::HTTPServerException)
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'parses the resulting feed into name/email pairs' do
|
83
|
+
@gmail.stubs(:get)
|
84
|
+
@gmail.expects(:response_body).returns(sample_xml('google-single'))
|
85
|
+
|
86
|
+
found = @gmail.contacts
|
87
|
+
found.size.should == 1
|
88
|
+
contact = found.first
|
89
|
+
contact.name.should == 'Fitzgerald'
|
90
|
+
contact.emails.should == [{'primary' => 'true', 'type' => 'home', 'value' => 'fubar@gmail.com'}]
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'parses a complex feed into name/email pairs' do
|
94
|
+
@gmail.stubs(:get)
|
95
|
+
@gmail.expects(:response_body).returns(sample_xml('google-many'))
|
96
|
+
|
97
|
+
found = @gmail.contacts
|
98
|
+
found.size.should == 4
|
99
|
+
found[0].name.should == 'Elizabeth Bennet'
|
100
|
+
found[0].emails.should == [{'primary' => 'true', 'type' => 'work', 'value' => 'liz@gmail.com'}, {'primary' => 'false', 'type' => 'home', 'value' => 'liz@example.org'}]
|
101
|
+
found[1].name.should == 'Poor Jack'
|
102
|
+
found[1].emails.should == []
|
103
|
+
found[2].name.should == 'William Paginate'
|
104
|
+
found[2].emails.should == [{'primary' => 'false', 'type' => 'other', 'value' => 'will_paginate@googlegroups.com'}]
|
105
|
+
found[3].name.should be_nil
|
106
|
+
found[3].emails.should == [{'primary' => 'false', 'type' => 'other', 'value' => 'anonymous@example.com'}]
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'makes modification time available after parsing' do
|
110
|
+
@gmail.updated_at.should be_nil
|
111
|
+
@gmail.stubs(:get)
|
112
|
+
@gmail.expects(:response_body).returns(sample_xml('google-single'))
|
113
|
+
|
114
|
+
@gmail.contacts
|
115
|
+
u = @gmail.updated_at
|
116
|
+
u.year.should == 2008
|
117
|
+
u.day.should == 5
|
118
|
+
@gmail.updated_at_string.should == '2008-03-05T12:36:38.836Z'
|
119
|
+
end
|
120
|
+
|
121
|
+
describe 'GET query parameter handling' do
|
122
|
+
|
123
|
+
before :each do
|
124
|
+
@gmail = create
|
125
|
+
@gmail.stubs(:response_body)
|
126
|
+
@gmail.stubs(:parse_contacts)
|
127
|
+
end
|
128
|
+
|
129
|
+
it 'abstracts ugly parameters behind nicer ones' do
|
130
|
+
expect_params 'max-results' => '25',
|
131
|
+
'orderby' => 'lastmodified',
|
132
|
+
'sortorder' => 'ascending',
|
133
|
+
'start-index' => '11',
|
134
|
+
'updated-min' => 'datetime'
|
135
|
+
|
136
|
+
@gmail.contacts :limit => 25,
|
137
|
+
:offset => 10,
|
138
|
+
:order => 'lastmodified',
|
139
|
+
:descending => false,
|
140
|
+
:updated_after => 'datetime'
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'should have implicit :descending with :order' do
|
144
|
+
expect_params 'orderby' => 'lastmodified',
|
145
|
+
'sortorder' => 'descending',
|
146
|
+
'max-results' => '200'
|
147
|
+
|
148
|
+
@gmail.contacts :order => 'lastmodified'
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'should have default :limit of 200' do
|
152
|
+
expect_params 'max-results' => '200'
|
153
|
+
@gmail.contacts
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'should skip nil values in parameters' do
|
157
|
+
expect_params 'start-index' => '1'
|
158
|
+
@gmail.contacts :limit => nil, :offset => 0
|
159
|
+
end
|
160
|
+
|
161
|
+
def expect_params(params)
|
162
|
+
query_string = Contacts::Google.query_string(params)
|
163
|
+
FakeWeb::register_uri(:get, "www.google.com/m8/feeds/contacts/default/thin?#{query_string}", {})
|
164
|
+
end
|
165
|
+
|
166
|
+
end
|
167
|
+
|
168
|
+
describe 'Retrieving all contacts (in chunks)' do
|
169
|
+
|
170
|
+
before :each do
|
171
|
+
@gmail = create
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'should make only one API call when no more is needed' do
|
175
|
+
@gmail.expects(:contacts).with(instance_of(Hash)).once.returns((0..8).to_a)
|
176
|
+
|
177
|
+
@gmail.all_contacts({}, 10).should == (0..8).to_a
|
178
|
+
end
|
179
|
+
|
180
|
+
it 'should make multiple calls to :contacts when needed' do
|
181
|
+
@gmail.expects(:contacts).with(has_entries(:offset => 0 , :limit => 10)).returns(( 0..9 ).to_a)
|
182
|
+
@gmail.expects(:contacts).with(has_entries(:offset => 10, :limit => 10)).returns((10..19).to_a)
|
183
|
+
@gmail.expects(:contacts).with(has_entries(:offset => 20, :limit => 10)).returns((20..24).to_a)
|
184
|
+
|
185
|
+
@gmail.all_contacts({}, 10).should == (0..24).to_a
|
186
|
+
end
|
187
|
+
|
188
|
+
it 'should make one extra API call when not sure whether there are more contacts' do
|
189
|
+
@gmail.expects(:contacts).with(has_entries(:offset => 0 , :limit => 10)).returns((0..9).to_a)
|
190
|
+
@gmail.expects(:contacts).with(has_entries(:offset => 10, :limit => 10)).returns([])
|
191
|
+
|
192
|
+
@gmail.all_contacts({}, 10).should == (0..9).to_a
|
193
|
+
end
|
194
|
+
|
195
|
+
end
|
196
|
+
|
197
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'spec'
|
3
|
+
require 'mocha'
|
4
|
+
require 'cgi'
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'fake_web'
|
8
|
+
rescue LoadError
|
9
|
+
puts "~> spec_helper: Could not load fakeweb gem."
|
10
|
+
puts "~> spec_helper: Please install it with `gem install fakeweb'."
|
11
|
+
exit -1
|
12
|
+
end
|
13
|
+
|
14
|
+
$LOAD_PATH<< File.join(File.dirname(__FILE__), '..', 'lib')
|
15
|
+
|
16
|
+
FakeWeb.allow_net_connect = false
|
17
|
+
|
18
|
+
module SampleFeeds
|
19
|
+
FEED_DIR = File.dirname(__FILE__) + '/feeds/'
|
20
|
+
|
21
|
+
def sample_xml(name)
|
22
|
+
File.read "#{FEED_DIR}#{name}.xml"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module HttpMocks
|
27
|
+
def mock_response(type = :success)
|
28
|
+
klass = case type
|
29
|
+
when :success then Net::HTTPSuccess
|
30
|
+
when :redirect then Net::HTTPRedirection
|
31
|
+
when :fail then Net::HTTPClientError
|
32
|
+
else type
|
33
|
+
end
|
34
|
+
|
35
|
+
klass.new(nil, nil, nil)
|
36
|
+
end
|
37
|
+
|
38
|
+
def mock_connection(ssl = true)
|
39
|
+
connection = mock('HTTP connection')
|
40
|
+
connection.stubs(:start)
|
41
|
+
connection.stubs(:finish)
|
42
|
+
if ssl
|
43
|
+
connection.expects(:use_ssl=).with(true)
|
44
|
+
connection.expects(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE)
|
45
|
+
end
|
46
|
+
connection
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
Spec::Runner.configure do |config|
|
51
|
+
config.include SampleFeeds, HttpMocks
|
52
|
+
# config.predicate_matchers[:swim] = :can_swim?
|
53
|
+
|
54
|
+
config.mock_with :mocha
|
55
|
+
end
|
56
|
+
|
57
|
+
module Mocha
|
58
|
+
module ParameterMatchers
|
59
|
+
def query_string(entries, partial = false)
|
60
|
+
QueryStringMatcher.new(entries, partial)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class QueryStringMatcher < Mocha::ParameterMatchers::Base
|
66
|
+
|
67
|
+
def initialize(entries, partial)
|
68
|
+
@entries = entries
|
69
|
+
@partial = partial
|
70
|
+
end
|
71
|
+
|
72
|
+
def matches?(available_parameters)
|
73
|
+
string = available_parameters.shift.split('?').last
|
74
|
+
broken = string.split('&').map { |pair| pair.split('=').map { |value| CGI.unescape(value) } }
|
75
|
+
hash = Hash[*broken.flatten]
|
76
|
+
|
77
|
+
if @partial
|
78
|
+
has_entry_matchers = @entries.map do |key, value|
|
79
|
+
Mocha::ParameterMatchers::HasEntry.new(key, value)
|
80
|
+
end
|
81
|
+
Mocha::ParameterMatchers::AllOf.new(*has_entry_matchers).matches?([hash])
|
82
|
+
else
|
83
|
+
@entries == hash
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def mocha_inspect
|
88
|
+
"query_string(#{@entries.mocha_inspect})"
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'contacts/windows_live'
|
2
|
+
|
3
|
+
describe Contacts::WindowsLive do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@path = Dir.getwd + '/spec/feeds/'
|
7
|
+
@wl = Contacts::WindowsLive.new(@path + 'contacts.yml')
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'parse the XML contacts document' do
|
11
|
+
contacts = Contacts::WindowsLive.parse_xml(contacts_xml)
|
12
|
+
contacts.size.should == 2
|
13
|
+
contacts[0].service_id == "abc"
|
14
|
+
contacts[0].name.should == "Mia Pia"
|
15
|
+
contacts[0].firstname.should == "Mia"
|
16
|
+
contacts[0].lastname.should == "Pia"
|
17
|
+
contacts[0].emails.should include("mia@hotmail.com", "othermia@yahoo.com")
|
18
|
+
contacts[0].phones.should include({"value"=>"(123) 123 1234", "type"=>"home"}, {"value"=>"(321) 555 1234", "type"=>"other"})
|
19
|
+
contacts[0].addresses.should include({"region"=>"CA", "country"=>"USA", "postalCode"=>"92123", "streetAddress"=>"123 Green St", "type"=>"home", "locality"=>"Middleville", "formatted"=>"123 Green St, Middleville, CA, 92123, USA"})
|
20
|
+
|
21
|
+
contacts[0].service_id == "def"
|
22
|
+
contacts[1].name.should == ""
|
23
|
+
contacts[1].firstname.should == nil
|
24
|
+
contacts[1].lastname.should == nil
|
25
|
+
contacts[1].emails.should include("marcus@hotmail.com")
|
26
|
+
contacts[1].phones.should be_empty
|
27
|
+
contacts[1].addresses.should be_empty
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should can be initialized by a YAML file' do
|
31
|
+
wll = @wl.instance_variable_get('@wll')
|
32
|
+
|
33
|
+
wll.appid.should == 'your_app_id'
|
34
|
+
wll.securityalgorithm.should == 'wsignin1.0'
|
35
|
+
wll.returnurl.should == 'http://yourserver.com/your_return_url'
|
36
|
+
end
|
37
|
+
|
38
|
+
def contacts_xml
|
39
|
+
File.open(@path + 'wl_full_contacts.xml', 'r+').read
|
40
|
+
end
|
41
|
+
end
|