neves-ruby_picasa 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,397 @@
1
+ require 'objectify_xml'
2
+ require 'objectify_xml/atom'
3
+ require 'cgi'
4
+ require 'net/http'
5
+ require 'net/https'
6
+ require File.join(File.dirname(__FILE__), 'ruby_picasa/types')
7
+
8
+ module RubyPicasa
9
+ VERSION = '0.2.3'
10
+
11
+ class PicasaError < StandardError
12
+ end
13
+
14
+ class PicasaTokenError < PicasaError
15
+ end
16
+ end
17
+
18
+ # == Authorization
19
+ #
20
+ # RubyPicasa makes authorizing a Rails app easy. It is a two step process:
21
+ #
22
+ # First redirect the user to the authorization url, if the user authorizes your
23
+ # application, Picasa will redirect the user back to the url you specify (in
24
+ # this case authorize_picasa_url).
25
+ #
26
+ # Next, pass the Rails request object to the authorize_token method which will
27
+ # make the api call to upgrade the token and if successful return an initialized
28
+ # Picasa session object ready to use. The token object can be retrieved from the
29
+ # token attribute.
30
+ #
31
+ # class PicasaController < ApplicationController
32
+ # def request_authorization
33
+ # redirect_to Picasa.authorization_url(authorize_picasa_url)
34
+ # end
35
+ #
36
+ # def authorize
37
+ # if Picasa.token_in_request?(request)
38
+ # begin
39
+ # picasa = Picasa.authorize_request(request)
40
+ # current_user.picasa_token = picasa.token
41
+ # current_user.save
42
+ # flash[:notice] = 'Picasa authorization complete'
43
+ # redirect_to picasa_path
44
+ # rescue PicasaTokenError => e
45
+ # #
46
+ # @error = e.message
47
+ # render
48
+ # end
49
+ # end
50
+ # end
51
+ # end
52
+ #
53
+ class Picasa
54
+ class << self
55
+ # The user must be redirected to this address to authorize the application
56
+ # to access their Picasa account. The token_from_request and
57
+ # authorize_request methods can be used to handle the resulting redirect
58
+ # from Picasa.
59
+ def authorization_url(return_to_url, request_session = true, secure = false, authsub_url = nil)
60
+ session = request_session ? '1' : '0'
61
+ secure = secure ? '1' : '0'
62
+ return_to_url = CGI.escape(return_to_url)
63
+ url = authsub_url || 'http://www.google.com/accounts/AuthSubRequest'
64
+ "#{ url }?scope=http%3A%2F%2F#{ host }%2Fdata%2F&session=#{ session }&secure=#{ secure }&next=#{ return_to_url }"
65
+ end
66
+
67
+ # Takes a Rails request object and extracts the token from it. This would
68
+ # happen in the action that is pointed to by the return_to_url argument
69
+ # when the authorization_url is created.
70
+ def token_from_request(request)
71
+ if token = request.parameters['token']
72
+ return token
73
+ else
74
+ raise RubyPicasa::PicasaTokenError, 'No Picasa authorization token was found.'
75
+ end
76
+ end
77
+
78
+ def token_in_request?(request)
79
+ request.parameters['token']
80
+ end
81
+
82
+ # Takes a Rails request object as in token_from_request, then makes the
83
+ # token authorization request to produce the permanent token. This will
84
+ # only work if request_session was true when you created the
85
+ # authorization_url.
86
+ def authorize_request(request)
87
+ p = Picasa.new(token_from_request(request))
88
+ p.authorize_token!
89
+ p
90
+ end
91
+
92
+ # The url to make requests to without the protocol or path.
93
+ def host
94
+ @host ||= 'picasaweb.google.com'
95
+ end
96
+
97
+ # In the unlikely event that you need to access this api on a different url,
98
+ # you can set it here. It defaults to picasaweb.google.com
99
+ def host=(h)
100
+ @host = h
101
+ end
102
+
103
+ # A simple test used to determine if a given resource id is it's full
104
+ # identifier url. This is not intended to be a general purpose method as the
105
+ # test is just a check for the http/https protocol prefix.
106
+ def is_url?(path)
107
+ path.to_s =~ %r{\Ahttps?://}
108
+ end
109
+
110
+ # For more on possible options and their meanings, see:
111
+ # http://code.google.com/apis/picasaweb/reference.html
112
+ #
113
+ # The following values are valid for the thumbsize and imgmax query
114
+ # parameters and are embeddable on a webpage. These images are available as
115
+ # both cropped(c) and uncropped(u) sizes by appending c or u to the size.
116
+ # As an example, to retrieve a 72 pixel image that is cropped, you would
117
+ # specify 72c, while to retrieve the uncropped image, you would specify 72u
118
+ # for the thumbsize or imgmax query parameter values.
119
+ #
120
+ # 32, 48, 64, 72, 144, 160
121
+ #
122
+ # The following values are valid for the thumbsize and imgmax query
123
+ # parameters and are embeddable on a webpage. These images are available as
124
+ # only uncropped(u) sizes by appending u to the size or just passing the
125
+ # size value without appending anything.
126
+ #
127
+ # 200, 288, 320, 400, 512, 576, 640, 720, 800
128
+ #
129
+ # The following values are valid for the thumbsize and imgmax query
130
+ # parameters and are not embeddable on a webpage. These image sizes are
131
+ # only available in uncropped format and are accessed using only the size
132
+ # (no u is appended to the size).
133
+ #
134
+ # 912, 1024, 1152, 1280, 1440, 1600
135
+ #
136
+ def path(args = {})
137
+ path, options = parse_url(args)
138
+ if path.nil?
139
+ path = ["/data/feed/api"]
140
+ if args[:user_id] == 'all'
141
+ path += ["all"]
142
+ else
143
+ path += ["user", CGI.escape(args[:user_id] || 'default')]
144
+ end
145
+ path += ['albumid', CGI.escape(args[:album_id])] if args[:album_id]
146
+ path += ['album', CGI.escape(args[:album])] if args[:album]
147
+ path = path.join('/')
148
+ end
149
+ options['kind'] = 'photo' if args[:recent_photos] or args[:album_id] or args[:album]
150
+ if args[:thumbsize] and not args[:thumbsize].split(/,/).all? { |s| RubyPicasa::Photo::VALID.include?(s) }
151
+ raise RubyPicasa::PicasaError, 'Invalid thumbsize.'
152
+ end
153
+ if args[:imgmax] and not RubyPicasa::Photo::VALID.include?(args[:imgmax])
154
+ raise RubyPicasa::PicasaError, 'Invalid imgmax.'
155
+ end
156
+ [:max_results, :start_index, :tag, :q, :kind,
157
+ :access, :thumbsize, :imgmax, :bbox, :l].each do |arg|
158
+ options[arg.to_s.dasherize] = args[arg] if args[arg]
159
+ end
160
+ if options.empty?
161
+ path
162
+ else
163
+ [path, options.map { |k, v| [k.to_s, CGI.escape(v.to_s)].join('=') }.join('&')].join('?')
164
+ end
165
+ end
166
+
167
+ # builder helper for creating a public RubyPicasa::User
168
+ def public_user(user_id, options = {})
169
+ Picasa.new.user(user_id, options)
170
+ end
171
+
172
+ # builder helper for creating a public RubyPicasa::Album using an album_id
173
+ def public_album_by_id(user_id, album_id, options = {})
174
+ options[:user_id] = user_id
175
+ Picasa.new.album(album_id, options)
176
+ end
177
+
178
+ # builder helper for creating a public RubyPicasa::Album using an album_name
179
+ def public_album_by_name(user_id, album_name, options = {})
180
+ options[:user_id] = user_id
181
+ Picasa.new.album_by_name(album_name, options)
182
+ end
183
+
184
+ private
185
+
186
+ # Extract the path and a hash of key/value pairs from a given url with
187
+ # optional query string.
188
+ def parse_url(args)
189
+ url = args[:url]
190
+ url ||= args[:user_id] if is_url?(args[:user_id])
191
+ url ||= args[:album_id] if is_url?(args[:album_id])
192
+ if url
193
+ uri = URI.parse(url)
194
+ path = uri.path
195
+ options = {}
196
+ if uri.query
197
+ uri.query.split('&').each do |query|
198
+ k, v = query.split('=')
199
+ options[k] = CGI.unescape(v)
200
+ end
201
+ end
202
+ [path, options]
203
+ else
204
+ [nil, {}]
205
+ end
206
+ end
207
+ end
208
+
209
+ # The AuthSub token currently in use.
210
+ attr_reader :token
211
+
212
+ def initialize(token = nil)
213
+ @token = token
214
+ @request_cache = {}
215
+ end
216
+
217
+ # Attempt to upgrade the current AuthSub token to a permanent one. This only
218
+ # works if the Picasa session is initialized with a single use token.
219
+ def authorize_token!
220
+ http = Net::HTTP.new("www.google.com", 443)
221
+ http.use_ssl = true
222
+ response = http.get('/accounts/accounts/AuthSubSessionToken', auth_header)
223
+ token = response.body.scan(/Token=(.*)/).flatten.compact.first
224
+ if token
225
+ @token = token
226
+ else
227
+ raise RubyPicasa::PicasaTokenError, 'The request to upgrade to a session token failed.'
228
+ end
229
+ @token
230
+ end
231
+
232
+ # Retrieve a RubyPicasa::User record including all user albums.
233
+ def user(user_id_or_url = nil, options = {})
234
+ options = make_options(:user_id, user_id_or_url, options)
235
+ get(options)
236
+ end
237
+
238
+ # Retrieve a RubyPicasa::Album record. If you pass an id or a feed url it will
239
+ # include all photos. If you pass an entry url, it will not include photos.
240
+ def album(album_id_or_url, options = {})
241
+ options = make_options(:album_id, album_id_or_url, options)
242
+ get(options)
243
+ end
244
+
245
+ # Retrieve a RubyPicasa::Album record by using the album name
246
+ def album_by_name(album_name, options = {})
247
+ options = make_options(:album, album_name, options)
248
+ get(options)
249
+ end
250
+
251
+ # This request does not require authentication. Returns a RubyPicasa::Search
252
+ # object containing the first 10 matches. You can call #next and #previous to
253
+ # navigate the paginated results on the Search object.
254
+ def search(q, options = {})
255
+ h = {}
256
+ h[:max_results] = 10
257
+ h[:user_id] = 'all'
258
+ h[:kind] = 'photo'
259
+ # merge options over h, but merge q over options
260
+ get(h.merge(options).merge(:q => q))
261
+ end
262
+
263
+ # Retrieve a RubyPicasa object determined by the type of xml results returned
264
+ # by Picasa. Any supported type of RubyPicasa resource can be requested with
265
+ # this method.
266
+ def get_url(url, options = {})
267
+ options = make_options(:url, url, options)
268
+ get(options)
269
+ end
270
+
271
+ # Retrieve a RubyPicasa::RecentPhotos object, essentially a User object which
272
+ # contains photos instead of albums.
273
+ def recent_photos(user_id_or_url, options = {})
274
+ options = make_options(:user_id, user_id_or_url, options)
275
+ options[:recent_photos] = true
276
+ get(options)
277
+ end
278
+
279
+ # Retrieves the user's albums and finds the first one with a matching title.
280
+ # Returns a RubyPicasa::Album object.
281
+ def album_by_title(title, options = {})
282
+ if a = user.albums.find { |a| title === a.title }
283
+ a.load options
284
+ end
285
+ end
286
+
287
+ # Returns the raw xml from Picasa. See the Picasa.path method for valid
288
+ # options.
289
+ def xml(options = {})
290
+ http = Net::HTTP.new(Picasa.host, 80)
291
+ path = Picasa.path(options)
292
+ response = http.get(path, auth_header)
293
+ if response.code =~ /20[01]/
294
+ response.body
295
+ end
296
+ end
297
+
298
+ private
299
+
300
+ # If the value parameter is a hash, treat it as the options hash, otherwise
301
+ # insert the value into the hash with the key specified.
302
+ #
303
+ # Uses merge to ensure that a new hash object is returned to prevent caller's
304
+ # has from accidentally being modified.
305
+ def make_options(key, value, options)
306
+ if value.is_a? Hash
307
+ {}.merge value
308
+ else
309
+ options ||= {}
310
+ options.merge(key => value)
311
+ end
312
+ end
313
+
314
+ # Combines the cached xml request with the class_from_xml factory. See the
315
+ # Picasa.path method for valid options.
316
+ def get(options = {})
317
+ with_cache(options) do |xml|
318
+ class_from_xml(xml)
319
+ end
320
+ end
321
+
322
+ # Returns the header data needed to make AuthSub requests.
323
+ def auth_header
324
+ if token
325
+ { "Authorization" => %{AuthSub token="#{ token }"} }
326
+ else
327
+ {}
328
+ end
329
+ end
330
+
331
+ # Caches the raw xml returned from the API. Keyed on request url.
332
+ def with_cache(options)
333
+ path = Picasa.path(options)
334
+ @request_cache.delete(path) if options[:reload]
335
+ xml = nil
336
+ if @request_cache.has_key? path
337
+ xml = @request_cache[path]
338
+ else
339
+ xml = @request_cache[path] = xml(options)
340
+ end
341
+ if xml
342
+ yield xml
343
+ end
344
+ end
345
+
346
+ # Returns the first xml element in the document (see
347
+ # Objectify::Xml.first_element) with the xml data types of the feed and first entry
348
+ # element in the document, used to determine which RubyPicasa object should
349
+ # be initialized to handle the data.
350
+ def xml_data(xml)
351
+ if xml = Objectify::Xml.first_element(xml)
352
+ # There is something wrong with Nokogiri xpath/css search with
353
+ # namespaces. If you are searching a document that has namespaces,
354
+ # it's impossible to match any elements in the root xmlns namespace.
355
+ # Matching just on attributes works though.
356
+ feed, entry = xml.search('//*[@term][@scheme]', xml.namespaces)
357
+ feed_scheme = feed['term'] if feed
358
+ entry_scheme = entry['term'] if entry
359
+ [xml, feed_scheme, entry_scheme]
360
+ end
361
+ end
362
+
363
+ # Initialize the correct RubyPicasa object depending on the type of feed and
364
+ # entries in the document.
365
+ def class_from_xml(xml)
366
+ xml, feed_scheme, entry_scheme = xml_data(xml)
367
+ if xml
368
+ r = case feed_scheme
369
+ when /#user$/
370
+ case entry_scheme
371
+ when /#album$/
372
+ RubyPicasa::User.new(xml, self)
373
+ when /#photo$/
374
+ RubyPicasa::RecentPhotos.new(xml, self)
375
+ end
376
+ when /#album$/
377
+ case entry_scheme
378
+ when nil, /#photo$/
379
+ RubyPicasa::Album.new(xml, self)
380
+ end
381
+ when /#photo$/
382
+ case entry_scheme
383
+ when /#photo$/
384
+ RubyPicasa::Search.new(xml, self)
385
+ when nil
386
+ RubyPicasa::Photo.new(xml, self)
387
+ end
388
+ end
389
+ if r
390
+ r.session = self
391
+ r
392
+ else
393
+ raise RubyPicasa::PicasaError, "Unknown feed type\n feed: #{ feed_scheme }\n entry: #{ entry_scheme }"
394
+ end
395
+ end
396
+ end
397
+ end
@@ -0,0 +1,303 @@
1
+ require File.join(File.dirname(__FILE__), '../spec_helper')
2
+
3
+ include RubyPicasa
4
+
5
+ describe 'a RubyPicasa document', :shared => true do
6
+ it 'should have a feed_id' do
7
+ @object.feed_id.should_not be_nil
8
+ end
9
+
10
+ it 'should have an author' do
11
+ unless @no_author
12
+ @object.author.should_not be_nil
13
+ @object.author.name.should == 'Liz'
14
+ @object.author.uri.should == 'http://picasaweb.google.com/liz'
15
+ end
16
+ end
17
+
18
+ it 'should get links by name' do
19
+ @object.link('abc').should be_nil
20
+ @object.link('self').href.should_not be_nil
21
+ end
22
+
23
+ it 'should do nothing for previous and next' do
24
+ @object.previous.should be_nil if @object.link('previous').nil?
25
+ @object.next.should be_nil if @object.link('next').nil?
26
+ end
27
+
28
+ it 'should get the feed' do
29
+ @object.session.expects(:get_url).with(@object.feed_id.
30
+ gsub(/entry/, 'feed').
31
+ gsub(/default/, 'liz'), {})
32
+ @object.feed
33
+ end
34
+
35
+ it 'should have links' do
36
+ @object.links.should_not be_empty
37
+ @object.links.each do |l|
38
+ l.should be_an_instance_of(Objectify::Atom::Link)
39
+ end
40
+ end
41
+
42
+ describe 'session' do
43
+ it 'should return @session' do
44
+ @object.session = :sess
45
+ @object.session.should == :sess
46
+ end
47
+
48
+ it 'should get the parent session' do
49
+ @object.session = nil
50
+ @parent.expects(:session).returns(:parent_sess)
51
+ @object.session.should == :parent_sess
52
+ end
53
+
54
+ it 'should be nil if no parent' do
55
+ @object.session = nil
56
+ @object.expects(:parent).returns nil
57
+ @object.session.should be_nil
58
+ end
59
+ end
60
+ end
61
+
62
+
63
+ describe User do
64
+ it_should_behave_like 'a RubyPicasa document'
65
+
66
+ before :all do
67
+ @xml = open_file('user.atom').read
68
+ end
69
+
70
+ before do
71
+ @parent = mock('parent')
72
+ @object = @user = User.new(@xml, @parent)
73
+ @user.session = mock('session')
74
+ end
75
+
76
+ it 'should have albums' do
77
+ @user.albums.length.should == 1
78
+ @user.albums.first.should be_an_instance_of(Album)
79
+ end
80
+ end
81
+
82
+ describe RecentPhotos do
83
+ it_should_behave_like 'a RubyPicasa document'
84
+
85
+ before :all do
86
+ @xml = open_file('recent.atom').read
87
+ end
88
+
89
+ before do
90
+ @parent = mock('parent')
91
+ @object = @album = RecentPhotos.new(@xml, @parent)
92
+ @album.session = mock('session')
93
+ end
94
+
95
+ it 'should have 1 photo' do
96
+ @album.photos.length.should == 1
97
+ @album.photos.first.should be_an_instance_of(Photo)
98
+ end
99
+
100
+ it 'should request next' do
101
+ @album.session.expects(:get_url).with('http://picasaweb.google.com/data/feed/api/user/liz?start-index=2&max-results=1&kind=photo').returns(:result)
102
+ @album.next.should == :result
103
+ end
104
+
105
+ it 'should not request previous on first page' do
106
+ @album.session.expects(:get_url).never
107
+ @album.previous.should be_nil
108
+ end
109
+ end
110
+
111
+ describe Album do
112
+ it_should_behave_like 'a RubyPicasa document'
113
+
114
+ before :all do
115
+ @xml = open_file('album.atom').read
116
+ end
117
+
118
+ before do
119
+ @parent = mock('parent')
120
+ @object = @album = Album.new(@xml, @parent)
121
+ @album.session = mock('session')
122
+ end
123
+
124
+ it 'should have a numeric id' do
125
+ @object.id.should_not be_nil
126
+ @object.id.to_s.should match(/\A\d+\Z/)
127
+ end
128
+
129
+ it 'should have 1 entry' do
130
+ @album.entries.length.should == 1
131
+ end
132
+
133
+ it 'should get links by name' do
134
+ @album.link('abc').should be_nil
135
+ @album.link('alternate').href.should == 'http://picasaweb.google.com/liz/Lolcats'
136
+ end
137
+
138
+ describe 'photos' do
139
+ it 'should use entries if available' do
140
+ @album.expects(:session).never
141
+ @album.photos.should == @album.entries
142
+ end
143
+
144
+ it 'should request photos if needed' do
145
+ @album.entries = []
146
+ new_album = mock('album', :entries => [:photo])
147
+ @album.session.expects(:get_url).with(@album.link(/feed/).href, {}).returns(new_album)
148
+ @album.photos.should == [:photo]
149
+ end
150
+
151
+ it 'should not request photos twice if there are none' do
152
+ @album.entries = []
153
+ new_album = mock('album', :entries => [])
154
+ @album.session.expects(:get_url).with(@album.link(/feed/).href, {}).times(1).returns(new_album)
155
+ @album.photos.should == []
156
+ # note that mocks are set to accept only one get_url request
157
+ @album.photos.should == []
158
+ end
159
+
160
+ it 'should not request photos if there is no session' do
161
+ @album.entries = []
162
+ @album.expects(:session).returns(nil)
163
+ @album.photos.should == []
164
+ end
165
+ end
166
+
167
+ it 'should be public' do
168
+ @album.public?.should be_true
169
+ end
170
+
171
+ it 'should not be private' do
172
+ @album.private?.should be_false
173
+ end
174
+
175
+ describe 'first Photo' do
176
+ before do
177
+ @photo = @album.entries.first
178
+ @photo.should be_an_instance_of(Photo)
179
+ end
180
+
181
+ it 'should have a parent' do
182
+ @photo.parent.should == @album
183
+ end
184
+
185
+ it 'should not have an author' do
186
+ @photo.author.should be_nil
187
+ end
188
+
189
+ it 'should have a content' do
190
+ @photo.content.should be_an_instance_of(PhotoUrl)
191
+ end
192
+
193
+ it 'should have 3 thumbnails' do
194
+ @photo.thumbnails.length.should == 3
195
+ @photo.thumbnails.each do |t|
196
+ t.should be_an_instance_of(ThumbnailUrl)
197
+ end
198
+ end
199
+
200
+ it 'should have a numeric id' do
201
+ @object.id.should_not be_nil
202
+ @object.id.to_s.should match(/\A\d+\Z/)
203
+ end
204
+
205
+ it 'should have a default url' do
206
+ @photo.url.should == 'http://lh5.ggpht.com/liz/SKXR5BoXabI/AAAAAAAAAzs/tJQefyM4mFw/invisible_bike.jpg'
207
+ end
208
+
209
+ it 'should have thumbnail urls' do
210
+ @photo.url('72').should == 'http://lh5.ggpht.com/liz/SKXR5BoXabI/AAAAAAAAAzs/tJQefyM4mFw/s72/invisible_bike.jpg'
211
+ end
212
+
213
+ it 'should have a default url with options true' do
214
+ @photo.url(nil, true).should == [
215
+ 'http://lh5.ggpht.com/liz/SKXR5BoXabI/AAAAAAAAAzs/tJQefyM4mFw/invisible_bike.jpg',
216
+ { :width => 410, :height => 295 }
217
+ ]
218
+ end
219
+
220
+ it 'should have a default url with options' do
221
+ @photo.url(nil, :id => 'p').should == [
222
+ 'http://lh5.ggpht.com/liz/SKXR5BoXabI/AAAAAAAAAzs/tJQefyM4mFw/invisible_bike.jpg',
223
+ { :width => 410, :height => 295, :id => 'p' }
224
+ ]
225
+ end
226
+
227
+ it 'should have a default url with options first' do
228
+ @photo.url(:id => 'p').should == [
229
+ 'http://lh5.ggpht.com/liz/SKXR5BoXabI/AAAAAAAAAzs/tJQefyM4mFw/invisible_bike.jpg',
230
+ { :width => 410, :height => 295, :id => 'p' }
231
+ ]
232
+ end
233
+
234
+ it 'should have thumbnail urls with options' do
235
+ @photo.url('72', {:class => 'x'}).should == [
236
+ 'http://lh5.ggpht.com/liz/SKXR5BoXabI/AAAAAAAAAzs/tJQefyM4mFw/s72/invisible_bike.jpg',
237
+ { :width => 72, :height => 52, :class => 'x' }
238
+ ]
239
+ end
240
+
241
+ it 'should have thumbnail info' do
242
+ @photo.thumbnail('72').width.should == 72
243
+ end
244
+
245
+ it 'should retrieve valid thumbnail info' do
246
+ photo = mock('photo')
247
+ thumb = mock('thumb')
248
+ photo.expects(:thumbnails).returns([thumb])
249
+ @photo.session.expects(:get_url).with('http://picasaweb.google.com/data/feed/api/user/liz/albumid/5228155363249705041/photoid/5234820919508560306',
250
+ {:thumbsize => '32c'}).returns(photo)
251
+ @photo.thumbnail('32c').should == thumb
252
+ end
253
+
254
+ it 'should retrieve valid thumbnail info and handle not found' do
255
+ @photo.session.expects(:get_url).with('http://picasaweb.google.com/data/feed/api/user/liz/albumid/5228155363249705041/photoid/5234820919508560306',
256
+ {:thumbsize => '32c'}).returns(nil)
257
+ @photo.thumbnail('32c').should be_nil
258
+ end
259
+
260
+ it 'should convert the thumbnail url into another size without crop' do
261
+ @photo.tb(160).should == "http://lh5.ggpht.com/liz/SKXR5BoXabI/AAAAAAAAAzs/tJQefyM4mFw/s160/invisible_bike.jpg"
262
+ end
263
+
264
+ it 'should convert the thumbnail url into another size with crop' do
265
+ @photo.tb(160, true).should == "http://lh5.ggpht.com/liz/SKXR5BoXabI/AAAAAAAAAzs/tJQefyM4mFw/s160-c/invisible_bike.jpg"
266
+ end
267
+ end
268
+ end
269
+
270
+ describe Search do
271
+ it_should_behave_like 'a RubyPicasa document'
272
+
273
+ before :all do
274
+ @xml = open_file('search.atom').read
275
+ end
276
+
277
+ before do
278
+ @no_author = true
279
+ @parent = mock('parent')
280
+ @object = @search = Search.new(@xml, @parent)
281
+ @search.session = mock('session')
282
+ end
283
+
284
+ it 'should have 1 entry' do
285
+ @search.entries.length.should == 1
286
+ @search.entries.first.should be_an_instance_of(Photo)
287
+ end
288
+
289
+ it 'should alias entries to photos' do
290
+ @search.photos.should == @search.entries
291
+ end
292
+
293
+ it 'should request next' do
294
+ @search.session.expects(:get_url).with('http://picasaweb.google.com/data/feed/api/all?q=puppy&start-index=3&max-results=1').returns(:result)
295
+ @search.next.should == :result
296
+ end
297
+
298
+ it 'should request previous' do
299
+ @search.session.expects(:get_url).with('http://picasaweb.google.com/data/feed/api/all?q=puppy&start-index=1&max-results=1').returns(:result)
300
+ @search.previous.should == :result
301
+ end
302
+ end
303
+