bitsa 0.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,80 @@
1
+ # Loads Contacts from Gmail into a <tt>ContactsCache</tt> object.
2
+ #
3
+ # Copyright (C) 2011 Colin Noel Bell.
4
+ #
5
+ # This file is part of Bitsa.
6
+ #
7
+ # Bitsa is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # This program is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
+
20
+ require "gdata"
21
+
22
+ module Bitsa #:nodoc:
23
+
24
+ # Loads Contacts from Gmail into a <tt>ContactsCache</tt> object.
25
+ class GmailContactsLoader
26
+
27
+ # Number of contacts to retrieve as a single chunk.
28
+ @@FETCH_SIZE = 25
29
+
30
+ # Ctor specifying the Gmail (or Google Apps) user name and password.
31
+ def initialize(user, pw) #, lifespan_days)
32
+ @user = user
33
+ @pw = pw
34
+ end
35
+
36
+ # Refresh the passsed <tt>ContactsCache</tt> with the latest contact
37
+ # changes/deletions from Gmail.
38
+ def update_cache(cache)
39
+ client = GData::Client::Contacts.new
40
+ client.clientlogin(@user, @pw)
41
+
42
+ idx = 1
43
+ until load_chunk(client, idx, cache) < @@FETCH_SIZE
44
+ idx += @@FETCH_SIZE
45
+ end
46
+ cache.source_last_modified = DateTime.now.to_s
47
+ cache.save
48
+ end
49
+
50
+ private
51
+
52
+ def load_chunk(client, idx, cache)
53
+ last_modified = nil
54
+ url = "http://www.google.com/m8/feeds/contacts/#{@user}/thin"
55
+ url += "?orderby=lastmodified"
56
+ url += "&showdeleted=true"
57
+ url += "&max-results=#{@@FETCH_SIZE}"
58
+ url += "&start-index=#{idx}"
59
+ url += "&updated-min=#{CGI.escape(cache.source_last_modified)}" if cache.source_last_modified
60
+ feed = client.get(url).to_xml
61
+ feed.elements.each('entry') do |entry|
62
+ name = entry.elements['title'].text
63
+ name ||= ""
64
+ gmail_id = entry.elements['id'].text
65
+ deleted = entry.elements['gd:deleted'] ? true : false
66
+ if deleted
67
+ cache.delete(gmail_id)
68
+ else
69
+ addresses = []
70
+ entry.each_element('gd:email') do | addr |
71
+ addresses << addr.attributes['address']
72
+ end
73
+ cache.update(gmail_id, name, addresses)
74
+ end
75
+ last_modified = entry.elements['updated'].text
76
+ end
77
+ feed.elements.count
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,50 @@
1
+ # Application settings.
2
+ #
3
+ # Copyright (C) 2011 Colin Noel Bell.
4
+ #
5
+ # This file is part of Bitsa.
6
+ #
7
+ # Bitsa is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # This program is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
+
20
+ module Bitsa #:nodoc:
21
+
22
+ # Application settings.
23
+ class Settings
24
+
25
+ # Login to use to connect to GMail.
26
+ attr_reader :login
27
+
28
+ # Password to use to connect to GMail.
29
+ attr_reader :password
30
+
31
+ # Path to file to store cached contact information in.
32
+ attr_reader :cache_file_path
33
+
34
+ # Load settings from a hash of data from the configuration file and
35
+ # options passed on the command line.
36
+ #
37
+ # Options passed on the command line override those in the
38
+ # configuration file.
39
+ def load(config_file_hash, options)
40
+ @login = config_file_hash.data[:login]
41
+ @password = config_file_hash.data[:password]
42
+ @cache_file_path = config_file_hash.data[:cache_file_path]
43
+
44
+ @login = options[:login] if options[:login]
45
+ @password = options[:password] if options[:password]
46
+ @cache_file_path = options[:cache_file_path] if options[:cache_file_path]
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,22 @@
1
+ # Application Version information.
2
+ #
3
+ # Copyright (C) 2011 Colin Noel Bell.
4
+ #
5
+ # This file is part of Bitsa.
6
+ #
7
+ # Bitsa is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # This program is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
+
20
+ module Bitsa #:nodoc:
21
+ VERSION = "0.10"
22
+ end
@@ -0,0 +1,66 @@
1
+ require "helper"
2
+ require "bitsa/args_processor"
3
+
4
+ describe Bitsa::ArgsProcessor do
5
+ context "when created" do
6
+ before(:all) { @ap = Bitsa::ArgsProcessor.new }
7
+
8
+ it "should recognise the 'update' command" do
9
+ @ap.parse(["update"])
10
+ end
11
+
12
+ it "should throw raise SystemExit if an invalid command passed" do
13
+ lambda {@ap.parse(['unknown'])}.should raise_error(SystemExit)
14
+ end
15
+
16
+ it "should throw raise SystemExit if nothing passed" do
17
+ lambda {@ap.parse([])}.should raise_error(SystemExit)
18
+ end
19
+
20
+ context "and being passed valid commands" do
21
+ [["reload"], ["search", "data"], ["update"]].each do |ar|
22
+ cmd = ar[0]
23
+ it "should recognise the #{cmd} command" do
24
+ @ap.parse(ar)
25
+ @ap.cmd.should == cmd
26
+ end
27
+ end
28
+ end
29
+
30
+ it "should recognise valid long arguments" do
31
+ args = []
32
+ args << "--config-file"
33
+ args << "somefile"
34
+ args << "--login"
35
+ args << "someone"
36
+ args << "--password"
37
+ args << "mypassword"
38
+ args << "update"
39
+
40
+ @ap.parse(args)
41
+
42
+ @ap.global_opts[:config_file].should == "somefile"
43
+ @ap.global_opts[:login].should == "someone"
44
+ @ap.global_opts[:password].should == "mypassword"
45
+ end
46
+
47
+ it "should recognise valid short arguments" do
48
+ args = []
49
+ args << "-c"
50
+ args << "somefile"
51
+ args << "-l"
52
+ args << "someone"
53
+ args << "-p"
54
+ args << "mypassword"
55
+ args << "update"
56
+
57
+ @ap.parse(args)
58
+
59
+ @ap.global_opts[:config_file].should == "somefile"
60
+ @ap.global_opts[:login].should == "someone"
61
+ @ap.global_opts[:password].should == "mypassword"
62
+ end
63
+
64
+ end
65
+
66
+ end
@@ -0,0 +1,32 @@
1
+ require "helper"
2
+ require "bitsa/config_file"
3
+
4
+ describe Bitsa::ConfigFile do
5
+ context "An existing configuration file" do
6
+ before(:all) { @config = Bitsa::ConfigFile.new("spec/data/config.yml") }
7
+ it "should read values from config file" do
8
+ @config.data[:login].should == "test@gmail.com"
9
+ @config.data[:password].should == "myPassword"
10
+ end
11
+ end
12
+
13
+ context "An non-existent configuration file" do
14
+ before(:all) { @config = Bitsa::ConfigFile.new("/tmp/i-dont-exist") }
15
+ it "should have no values" do
16
+ @config.data.should == {}
17
+ end
18
+ end
19
+
20
+ context "An existing configuration file that I have no read rights to" do
21
+ before(:all) do
22
+ @tmp_file = Tempfile.open("cache")
23
+ FileUtils.cp("spec/data/config.yml", @tmp_file.path)
24
+ FileUtils.chmod(0222, @tmp_file.path)
25
+ end
26
+
27
+ it "should throw an exception when I try to read from it" do
28
+ lambda { Bitsa::ConfigFile.new(@tmp_file.path) }.should raise_error(Errno::EACCES)
29
+ end
30
+ end
31
+
32
+ end
@@ -0,0 +1,294 @@
1
+ require "fileutils"
2
+ require "tempfile"
3
+
4
+ require "helper"
5
+
6
+ require "bitsa/contacts_cache"
7
+
8
+ describe Bitsa::ContactsCache do
9
+ context "cache" do
10
+ it "should build successfully if cache file empty" do
11
+ tmp_file = create_empty_temp_file
12
+ cache = Bitsa::ContactsCache.new(tmp_file.path, 1)
13
+ should_be_an_empty_cache cache
14
+ end
15
+
16
+ it "should build successfully if cache file does not exist" do
17
+ cache = Bitsa::ContactsCache.new("/tmp/idonotexististhatoky", 1)
18
+ should_be_an_empty_cache cache
19
+ end
20
+
21
+ it "should fail with exception if user not authorised to read cache file" do
22
+ tmp_file = create_empty_temp_file
23
+ FileUtils.chmod(0222, tmp_file.path)
24
+ lambda { Bitsa::ContactsCache.new(tmp_file.path, 1).should raise_error(Errno::EACCES) }
25
+ end
26
+
27
+ it "should not be stale if last updated today" do
28
+ create_cache(DateTime.now, 1)
29
+ @cache.stale?.should_not be_true
30
+ end
31
+
32
+ it "should be stale if last updated yesterday and lifespan is 1 day" do
33
+ create_cache(DateTime.now-1, 1)
34
+ @cache.stale?.should be_true
35
+ end
36
+
37
+ it "should not be stale if last updated yesterday and lifespan is 2 days" do
38
+ create_cache(DateTime.now-1, 2)
39
+ @cache.stale?.should_not be_true
40
+ end
41
+ end
42
+
43
+ context "clearing the cache" do
44
+ before(:each) do
45
+ create_cache
46
+ @cache.should have_at_least(1).entries
47
+ @cache.empty?.should be_false
48
+ end
49
+
50
+ it "should leave the cache empty" do
51
+ @cache.clear!
52
+ @cache.size.should == 0
53
+ @cache.empty?.should be_true
54
+ end
55
+ end
56
+
57
+ context "searching the test data" do
58
+ # 4 contacts with 5 email addresses
59
+ before(:all) { create_cache }
60
+
61
+ it "should have read the correct number of contacts" do
62
+ @cache.size.should == 4
63
+ end
64
+
65
+ it "should return all entries if blank searched for" do
66
+ results = @cache.search('')
67
+ results.size.should == 5
68
+ results.flatten(0).sort.should == read_test_data
69
+ end
70
+
71
+ it "should return all entries if a nill string searched for" do
72
+ results = @cache.search(nil)
73
+ results.size.should == 5
74
+ results.flatten(0).sort.should == read_test_data
75
+ end
76
+
77
+ it "should find correctly when searching by start of email address" do
78
+ results = @cache.search('test1')
79
+ results.should =~ [["test1@example.com", "My Tester"]]
80
+
81
+ results = @cache.search('jo')
82
+ expected = [["john_smith@here.org", ""], ["Joan.bloggs@somewhere.com.au", "Joan Bloggshere"]]
83
+ results.should =~ expected
84
+ end
85
+
86
+ it "should find correctly when searching by end of email address" do
87
+ results = @cache.search('org')
88
+ expected = [["john_smith@here.org", ""]]
89
+ results.should =~ expected
90
+ end
91
+
92
+ it "should find correctly when searching by middle of email address" do
93
+ results = @cache.search('n_s')
94
+ expected = [["john_smith@here.org", ""]]
95
+ results.should =~ expected
96
+ end
97
+
98
+ it "should find correctly when searching by email address irrespective of case" do
99
+ results = @cache.search('N_sMI')
100
+ expected = [["john_smith@here.org", ""]]
101
+ results.should =~ expected
102
+ end
103
+
104
+ it "should find correctly when searching by start of name" do
105
+ results = @cache.search('Joan Blo')
106
+ expected = [["Joan.bloggs@somewhere.com.au", "Joan Bloggshere"]]
107
+ results.should =~ expected
108
+ end
109
+
110
+ it "should find correctly when searching by end of name" do
111
+ results = @cache.search('ggshere')
112
+ expected = [["Joan.bloggs@somewhere.com.au", "Joan Bloggshere"]]
113
+ results.should =~ expected
114
+ end
115
+
116
+ it "should find correctly when searching by middle of name" do
117
+ results = @cache.search('n Bl')
118
+ expected = [["Joan.bloggs@somewhere.com.au", "Joan Bloggshere"]]
119
+ results.should =~ expected
120
+ end
121
+
122
+ it "should find correctly when searching by name irrespective of case" do
123
+ results = @cache.search('N BL')
124
+ expected = [["Joan.bloggs@somewhere.com.au", "Joan Bloggshere"]]
125
+ results.should =~ expected
126
+ end
127
+
128
+ it "should find no results if nothing to find" do
129
+ results = @cache.search('nothing is here')
130
+ results.size.should == 0
131
+ end
132
+
133
+ it "should return all email addresses on a contact with multiple email addresses when matching on name" do
134
+ results = @cache.search('multip')
135
+ expected = [["email2@somewhere.com", "Mr Multiple"], ["email1@somewhere.com", "Mr Multiple"]]
136
+ results.should =~ expected
137
+ end
138
+
139
+ it "should return only the matched email address on a contact with multiple email addresses" do
140
+ results = @cache.search('email1')
141
+ expected = [["email1@somewhere.com", "Mr Multiple"]]
142
+ results.should =~ expected
143
+ end
144
+
145
+ it "should find by ID correctly" do
146
+ id = "http://www.google.com/m8/feeds/contacts/person%40example.org/base/685e301a549c176e"
147
+ results = @cache.get(id)
148
+ expected = [["email1@somewhere.com", "Mr Multiple"],
149
+ ["email2@somewhere.com", "Mr Multiple"]]
150
+ results.should =~ expected
151
+ end
152
+
153
+ it "should return nil if finding by a non-existent ID" do
154
+ id = "http://www.google.com/m8/feeds/contacts/person%40example.org/base/4783783783"
155
+ @cache.get(id).should be_nil
156
+ end
157
+ end
158
+
159
+ context "updating the test data" do
160
+ # 4 contacts with 5 email addresses
161
+ before(:each) { create_cache }
162
+
163
+ it "should handle update with a single email address correctly" do
164
+ id = "http://www.google.com/m8/feeds/contacts/person%40example.org/base/637e301a549c176e"
165
+ results = @cache.get(id)
166
+ expected = [["Joan.bloggs@somewhere.com.au", "Joan Bloggshere"]]
167
+ results.should =~ expected
168
+
169
+ @cache.update(id, "Tammy Smith", ["tammy5@example.com"])
170
+ results = @cache.get(id)
171
+ expected = [["tammy5@example.com", "Tammy Smith"]]
172
+ results.should =~ expected
173
+ end
174
+
175
+ it "should handle update with two email addresses correctly" do
176
+ id = "http://www.google.com/m8/feeds/contacts/person%40example.org/base/637e301a549c176e"
177
+ results = @cache.get(id)
178
+ expected = [["Joan.bloggs@somewhere.com.au", "Joan Bloggshere"]]
179
+ results.should =~ expected
180
+
181
+ @cache.update(id, "Tammy Smith", ["tammy5@example.com", "smithtammy@exampel.org"])
182
+ results = @cache.get(id)
183
+ expected = [["smithtammy@exampel.org", "Tammy Smith"], ["tammy5@example.com", "Tammy Smith"]]
184
+ results.should =~ expected
185
+ end
186
+ end
187
+
188
+ context "deleting a non-existent entry" do
189
+ before(:each) { create_cache }
190
+
191
+ it "should no longer contain the deleted entry" do
192
+ @cache.delete("NONEXISTENT")
193
+ @cache.get("NONEXISTENT").should be_nil
194
+ end
195
+
196
+ it "should not change the number of entries in the cache" do
197
+ lambda {
198
+ @cache.delete("NONEXISTENT")
199
+ }.should_not change(@cache, :size)
200
+ end
201
+
202
+ it "should return nil" do
203
+ @cache.delete("NONEXISTENT").should be_nil
204
+ end
205
+ end
206
+
207
+ context "deleting an existing entry" do
208
+ before(:each) do
209
+ create_cache
210
+ @id = "http://www.google.com/m8/feeds/contacts/person%40example.org/base/637e301a549c176e"
211
+ end
212
+
213
+ it "should_change the number of entries by -1" do
214
+ lambda {
215
+ @cache.delete(@id)
216
+ }.should change(@cache, :size).by(-1)
217
+ end
218
+
219
+ it "should return the deleted entry" do
220
+ results = @cache.delete(@id)
221
+ expected = [["Joan.bloggs@somewhere.com.au", "Joan Bloggshere"]]
222
+ results.should =~ expected
223
+ end
224
+
225
+ it "should no longer contain the deleted entry" do
226
+ @cache.delete(@id)
227
+ @cache.get(@id).should be_nil
228
+ end
229
+ end
230
+
231
+ context "saving the test data" do
232
+ before(:each) { create_cache }
233
+ it "should save changes to existing contacts" do
234
+ id = "http://www.google.com/m8/feeds/contacts/person%40example.org/base/637e301a549c176e"
235
+ @cache.update(id, "The Changed Name", ["change1@somewhere.org"])
236
+ @cache.save
237
+ new_cache = Bitsa::ContactsCache.new(@tmp_file.path, 1)
238
+ expected = [["change1@somewhere.org", "The Changed Name"]]
239
+ new_cache.get(id).should =~ expected
240
+ end
241
+
242
+ it "should save newly added entries" do
243
+ id = "NONEXISTENT"
244
+ @cache.update(id, "The Changed Name", ["change1@somewhere.org"])
245
+ @cache.save
246
+ new_cache = Bitsa::ContactsCache.new(@tmp_file.path, 1)
247
+ expected = [["change1@somewhere.org", "The Changed Name"]]
248
+ new_cache.get(id).should =~ expected
249
+ end
250
+
251
+ context "to a file that I have no write rights to" do
252
+ before(:each) { FileUtils.chmod(0444, @tmp_file.path) }
253
+
254
+ it "should throw an exception when I try to write to it" do
255
+ lambda {@cache.save}.should raise_error(Errno::EACCES)
256
+ end
257
+
258
+ end
259
+ end
260
+
261
+ private
262
+
263
+ def create_cache(last_modified = nil, lifespan_days = 1)
264
+ source_last_modified, addresses = YAML::load_file("spec/data/bitsa_cache.yml")
265
+ source_last_modified = last_modified.to_s if last_modified
266
+
267
+ @tmp_file = Tempfile.open("cache")
268
+ File.open(@tmp_file.path, "w") do |f|
269
+ f.write(YAML::dump([source_last_modified, addresses]))
270
+ end
271
+ @cache = Bitsa::ContactsCache.new(@tmp_file.path, lifespan_days)
272
+ end
273
+
274
+ def create_empty_temp_file
275
+ tmp_file = Tempfile.open("cache")
276
+ File.open(tmp_file.path, "w") do |f|
277
+ f.write('')
278
+ end
279
+ tmp_file
280
+ end
281
+
282
+ def should_be_an_empty_cache cache
283
+ cache.should_not be_nil
284
+ cache.size.should == 0
285
+ cache.source_last_modified.should be_nil
286
+ cache.stale?.should be_true
287
+ end
288
+
289
+ def read_test_data
290
+ source_last_modified, addresses = YAML::load_file(@tmp_file.path)
291
+ addresses.values.flatten(1).sort
292
+ end
293
+
294
+ end