neves-ruby_picasa 0.2.3

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.
@@ -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
+