pietern-simpleflickr 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ == 0.2.2 2009-01-26
2
+
3
+ * Moved to Jeweler for gemspec managment.
4
+
5
+ == 0.2.1 2009-01-13
6
+
7
+ * Used newgem to generate a Rakefile that is used to build a gemspec.
@@ -0,0 +1,21 @@
1
+ History.txt
2
+ MIT-LICENSE
3
+ Manifest.txt
4
+ README.rdoc
5
+ Rakefile
6
+ lib/simpleflickr.rb
7
+ lib/simpleflickr/authentication/authentication.rb
8
+ lib/simpleflickr/authentication/desktop.rb
9
+ lib/simpleflickr/authentication/web.rb
10
+ lib/simpleflickr/base.rb
11
+ lib/simpleflickr/http.rb
12
+ lib/simpleflickr/query.rb
13
+ simpleflickr.gemspec
14
+ spec/feeds/auth.getFrob.xml
15
+ spec/feeds/auth.getToken.xml
16
+ spec/flickr/auth_spec.rb
17
+ spec/flickr/base_spec.rb
18
+ spec/flickr/query_spec.rb
19
+ spec/rcov.opts
20
+ spec/spec.opts
21
+ spec/spec_helper.rb
@@ -0,0 +1,140 @@
1
+ = Introduction
2
+ Made as an abstraction to the Flickr API in an extensible way. Where other libraries fully implement the API, this one doesn't. This library signs your requests and performs the HTTP request. Furthermore, it provides you an easy way to authenticate your users.
3
+
4
+ = Usage
5
+ SimpleFlickr allows you to do authenticated and non-authenticated requests. If you only plan to do non-authenticated requests, skip the section on authentication and read on at "Example request".
6
+
7
+ In either case, assume we have the following class defined:
8
+ class SomeClient < SimpleFlickr::Base
9
+ api_key 'my_key'
10
+ secret 'my_secret'
11
+ end
12
+
13
+ == Authentication
14
+ For doing authenticated requests you first need to request a token for the user. SimpleFlickr implements two methods of Flickr authentication, being web- and desktop-authentication.
15
+
16
+ You can read up on the process at http://www.flickr.com/services/api/misc.userauth.html
17
+
18
+ For both methods, you need to generate an URL for the user to visit and authenticate your app. When generating this URL, you can specify what access level you need. This can be either <tt>:read</tt>, <tt>:write</tt>, or <tt>:delete</tt>, and defaults to <tt>:read</tt>.
19
+
20
+ === Web Authentication
21
+
22
+ web_auth = SomeClient.web_authentication
23
+ web_auth.authentication_url(:write)
24
+ # => "http://www.flickr.com/services/auth/?..."
25
+
26
+ When the user authenticated your app, he/she get redirected to your callback URL.
27
+ You can pass this method either the frob in the URL (<tt>params[:frob]</tt>), or
28
+ the entire URL (<tt>request.request_uri</tt>). Assuming this is not part of your original
29
+ request/response cycle, you may need to re-instantiate the +WebAuthentication+ object.
30
+
31
+ web_auth = SomeClient.web_authentication
32
+ token = web_auth.get_token(params[:frob])
33
+
34
+ === Desktop authentication
35
+
36
+ desktop_auth = SomeClient.desktop_authentication
37
+ desktop_auth.authentication_url(:write)
38
+ # => "http://www.flickr.com/services/auth/?..."
39
+
40
+ When the user authenticated your app, he/she needs to inform your app that the authentication
41
+ is complete. Then your app can continue requesting a token from Flickr that allows you to
42
+ perform authenticated requests.
43
+
44
+ This method uses the previously generated frob that was stored in the +DesktopAuthentication+
45
+ object to request a token. If you delete this instance in between, you can specify the frob to request
46
+ a token for by passing it as an argument.
47
+
48
+ So, if your +desktop_auth+ object is still alive, invoke:
49
+ token = desktop_auth.get_token
50
+ Otherwise, first store the frob after generating the authentication URL, and pass it to the +get_token+ method later:
51
+ frob_to_store = desktop_auth.frob
52
+
53
+ # The app quits here, or you do some other magic.
54
+
55
+ desktop_auth = SomeClient.desktop_authentication
56
+ token = desktop_auth.get_token(frob_that_you_stored_somewhere)
57
+
58
+ == Example request
59
+ Ofcourse, if we do non-authenticated requests (like searching all publicly available photos), we don't need to use a token. This would look like so:
60
+
61
+ client = SomeClient.new
62
+ client.get 'photos.search' do |params|
63
+ params.tags = ['drinking', 'beer'].join(',')
64
+ end
65
+
66
+ While this is quite self-explanatory, I'll elaborate a bit. You can call the <tt>#get</tt> method with the action to call (refer to the Flickr API which methods are available). As all Flickr methods start with <tt>flickr.</tt>, you can leave that out for readability. If you specify a block, you can define parameters for the call. Any parameter can be called as an accessor.
67
+
68
+ The method returns a Hpricot XML object. For a full reference to traversal of this object, please look into the Hpricot docs. But to given you an impression:
69
+
70
+ doc = client.get 'photos.search' do |params|
71
+ params.tags = ['drinking', 'beer'].join(',')
72
+ end
73
+
74
+ (doc / 'photo').collect { |p| [p[:title], p[:id]] }
75
+ # => [["DSC09104", "3193174165"], ["DSC09130", "3193173953"], ...]
76
+
77
+ == Example authenticated request
78
+ Making an authenticated request differs from non-authenticated requests in one way only, being that you need to specify the token to use. This can be done in a number of ways.
79
+
80
+ Instantiate your client with the token:
81
+ client = SomeClient.new(token)
82
+
83
+ Instantiate a client without a token and specify it when doing a method-call:
84
+ client = SomeClient.new
85
+ client.with_token(token) do
86
+ get 'photos.search' do
87
+ ...
88
+ end
89
+ end
90
+
91
+ Don't instantiate and use the provided class-method:
92
+ SomeClient.with_token(token) do
93
+ get 'photos.search' do
94
+ ...
95
+ end
96
+ end
97
+
98
+ It is even possible to scope calls to a token like this:
99
+ client.with_token(token_of_user1) do
100
+ # stuff for user1
101
+ with_token(token_of_user2) do
102
+ # other stuff for user2
103
+ end
104
+ # some more stuff for user1
105
+ end
106
+
107
+ An example call for getting all private photos for a user would look like this:
108
+ SomeClient.with_token(token_of_user) do
109
+ get 'photos.search' do |params|
110
+ # Look into the Flickr API docs for a full reference of possible parameters
111
+ # http://www.flickr.com/services/api/flickr.photos.search.html
112
+ params.user_id = :me
113
+ params.privacy_level = 5
114
+ end
115
+ end
116
+
117
+ = Author
118
+ * Pieter Noordhuis (pcnoordhuis@gmail.com)
119
+
120
+ = License
121
+ Copyright (c) 2008 Pieter Noordhuis
122
+
123
+ Permission is hereby granted, free of charge, to any person obtaining
124
+ a copy of this software and associated documentation files (the
125
+ "Software"), to deal in the Software without restriction, including
126
+ without limitation the rights to use, copy, modify, merge, publish,
127
+ distribute, sublicense, and/or sell copies of the Software, and to
128
+ permit persons to whom the Software is furnished to do so, subject to
129
+ the following conditions:
130
+
131
+ The above copyright notice and this permission notice shall be
132
+ included in all copies or substantial portions of the Software.
133
+
134
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
135
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
136
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
137
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
138
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
139
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
140
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,4 @@
1
+ ---
2
+ :patch: 2
3
+ :major: 0
4
+ :minor: 2
@@ -0,0 +1,28 @@
1
+ require 'net/http'
2
+ require 'digest/md5'
3
+ require 'rubygems'
4
+ require 'hpricot'
5
+
6
+ $:.unshift File.join File.dirname(__FILE__), 'simpleflickr'
7
+ require 'query'
8
+ require 'http'
9
+ require 'base'
10
+
11
+ require 'authentication/authentication'
12
+ require 'authentication/web'
13
+ require 'authentication/desktop'
14
+
15
+ SimpleFlickr::Authentication.class_eval do
16
+ include SimpleFlickr::Query
17
+ end
18
+
19
+ module SimpleFlickr
20
+ @verbose = false
21
+ def self.verbose?
22
+ @verbose
23
+ end
24
+
25
+ def self.verbose=(v)
26
+ @verbose = !!v
27
+ end
28
+ end
@@ -0,0 +1,38 @@
1
+
2
+ module SimpleFlickr
3
+ class Authentication
4
+
5
+ AuthenticationService = 'http://www.flickr.com/services/auth/?'
6
+ attr_reader :permission
7
+
8
+ def initialize(options)
9
+ # This class must be subclassed
10
+ if self.class.name =~ /::Authentication$/
11
+ raise "The Authentication class must be subclassed"
12
+ end
13
+
14
+ unless options.key?(:api_key) && options.key?(:secret)
15
+ raise ArgumentError.new("Options should contain at least :api_key and :secret")
16
+ end
17
+
18
+ @permission = (options[:permission] || :read).to_sym
19
+ @default_params = { :api_key => options[:api_key], :secret => options[:secret] }
20
+ end
21
+
22
+ def token_url(frob)
23
+ params = default_params.merge(:frob => frob, :method => 'flickr.auth.getToken')
24
+ url_for(params)
25
+ end
26
+
27
+ def get_token_from_frob(frob)
28
+ response = HTTP.start do |flickr|
29
+ flickr.get(token_url(frob))
30
+ end
31
+ doc = Hpricot::XML response.body
32
+ doc.at('token').inner_text
33
+ end
34
+
35
+ private
36
+ attr_reader :default_params
37
+ end
38
+ end
@@ -0,0 +1,41 @@
1
+ module SimpleFlickr
2
+ class DesktopAuthentication < Authentication
3
+
4
+ # Returns URL where to get a frob.
5
+ def frob_url
6
+ url_for(default_params.merge(:method => 'flickr.auth.getFrob'))
7
+ end
8
+
9
+ # Returns the frob from the response
10
+ def frob_from_response(response)
11
+ doc = Hpricot::XML response.body
12
+ doc.at('frob').inner_text
13
+ end
14
+
15
+ # Constructs an URL for the user to go to and authenticate
16
+ def authentication_url_for_frob(frob)
17
+ params = default_params.merge(:perms => 'read', :frob => frob)
18
+ AuthenticationService + query_string(params)
19
+ end
20
+
21
+ # Goes through the process of requesting a frob and constructing
22
+ # an authentication URL for the user all in one call
23
+ def authentication_url
24
+ response = HTTP.start do |flickr|
25
+ flickr.get(frob_url)
26
+ end
27
+ @frob = frob_from_response(response)
28
+ authentication_url_for_frob(@frob)
29
+ end
30
+
31
+ # Tries to get a token from the previously used frob.
32
+ def get_token(arg = nil)
33
+ raise "Please specify a frob to use" if frob.nil? && arg.nil?
34
+ get_token_from_frob(frob.nil? ? arg : frob)
35
+ end
36
+
37
+ private
38
+ attr_reader :frob
39
+
40
+ end
41
+ end
@@ -0,0 +1,22 @@
1
+ module SimpleFlickr
2
+ class WebAuthentication < Authentication
3
+
4
+ def authentication_url
5
+ params = default_params.merge(:perms => @permission)
6
+ AuthenticationService + query_string(params)
7
+ end
8
+
9
+ # Tries to get a frob from an URL
10
+ def get_frob_from_url(url)
11
+ url.to_s[/\?frob=([0-9a-f-]+)$/i, 1]
12
+ end
13
+
14
+ # Gets a token from the frob. The frob or the callback
15
+ # URL can be given.
16
+ def get_token(input)
17
+ input = get_frob_from_url(input) || input
18
+ get_token_from_frob(input)
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,100 @@
1
+ module SimpleFlickr
2
+ class Base
3
+ @@params = {}
4
+
5
+ def self.default_params
6
+ @@params
7
+ end
8
+
9
+ def self.api_key(key)
10
+ @@params[:api_key] = key
11
+ end
12
+
13
+ def self.secret(secret)
14
+ @@params[:secret] = secret
15
+ end
16
+
17
+ private_class_method :api_key, :secret
18
+
19
+ def self.authentication_hash(perm)
20
+ unless perm.nil? || [:read, :write, :delete].include?(perm)
21
+ raise ArgumentError.new("Given permission is not one of :read, :write or :delete")
22
+ end
23
+ default_params.merge :permission => (perm || :read)
24
+ end
25
+
26
+ def self.web_authentication(perm = nil)
27
+ WebAuthentication.new authentication_hash(perm)
28
+ end
29
+
30
+ def self.desktop_authentication(perm = nil)
31
+ DesktopAuthentication.new authentication_hash(perm)
32
+ end
33
+
34
+ # Token scoping on class
35
+ def self.with_token(t, &block)
36
+ self.new(t).instance_eval &block
37
+ end
38
+
39
+ # Token scoping on instance
40
+ def with_token(t, &block)
41
+ @tokens.push t
42
+ instance_eval &block
43
+ @tokens.pop
44
+ end
45
+
46
+ def initialize(token = nil)
47
+ # Store tokens in an array for scoping purposes
48
+ @tokens = [token].compact
49
+ end
50
+
51
+ def token
52
+ @tokens.last
53
+ end
54
+
55
+ def params_hash
56
+ params = self.class.default_params
57
+ if token.nil?
58
+ params
59
+ else
60
+ params.merge(:auth_token => token)
61
+ end
62
+ end
63
+
64
+ def get(method, &block)
65
+ params = Ext.hash_proxy params_hash.merge(:method => "flickr.#{method}"), &block
66
+
67
+ response = HTTP.start do |flickr|
68
+ flickr.get QueryBuilder.url_for(params)
69
+ end
70
+ xml = Hpricot::XML response.body
71
+ end
72
+
73
+ # Let the included methods from the Query module live in another
74
+ # namespace to prevent subclasses from overriding them.
75
+ class QueryBuilder
76
+ extend Query
77
+ end
78
+
79
+ class Ext
80
+ def self.hash_proxy(hsh)
81
+ # Adds a proxy via method_missing for easy access to hash attributes
82
+ hsh = hsh.dup
83
+ class << hsh
84
+ def method_missing(sym, *args)
85
+ if property = sym.to_s[/^(\w+)=$/, 1]
86
+ key = property.to_sym
87
+ raise ArgumentError.new("Key #{key.inspect} already defined") unless self[key].nil?
88
+ self[key] = args.first.to_s
89
+ else
90
+ super
91
+ end
92
+ end
93
+ end
94
+
95
+ yield hsh
96
+ hsh
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,36 @@
1
+
2
+ module SimpleFlickr
3
+ module HTTP
4
+ DOMAIN = 'api.flickr.com'
5
+
6
+ def self.start
7
+ http = Net::HTTP.new(DOMAIN)
8
+ http.start
9
+
10
+ begin
11
+ response = yield(http)
12
+ inspect_response(response) if verbose?
13
+
14
+ if response.is_a?(Net::HTTPSuccess)
15
+ response
16
+ else
17
+ response.error!
18
+ end
19
+ ensure
20
+ http.finish
21
+ end
22
+ end
23
+
24
+ def self.inspect_response(response, out = $stderr)
25
+ out.puts response.inspect
26
+ for name, value in response
27
+ out.puts "#{name}: #{value}"
28
+ end
29
+ out.puts "----\n#{response.body}\n----" unless response.body.empty?
30
+ end
31
+
32
+ def self.verbose?
33
+ SimpleFlickr::verbose?
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,40 @@
1
+
2
+ module SimpleFlickr
3
+ module Query
4
+ ServicesPath = '/services/rest/'
5
+
6
+ # Use the key-sorted version of the parameters to construct
7
+ # a string, to which the secret is prepended.
8
+ def sort_params(params)
9
+ params.sort do |a,b|
10
+ a.to_s <=> b.to_s
11
+ end
12
+ end
13
+
14
+ def string_to_sign(params, secret)
15
+ string_to_sign = secret + sort_params(params).inject('') do |str, pair|
16
+ key, value = pair
17
+ str + key.to_s + value.to_s
18
+ end
19
+ end
20
+
21
+ # Get the MD5 digest of the string to sign
22
+ def signature(params, secret)
23
+ Digest::MD5.hexdigest(string_to_sign(params, secret))
24
+ end
25
+
26
+ def query_string(params)
27
+ secret = params.delete(:secret)
28
+ params[:api_sig] = signature(params, secret)
29
+
30
+ params.inject([]) do |arr, pair|
31
+ key, value = pair
32
+ arr << "#{key}=#{value}"
33
+ end.join('&')
34
+ end
35
+
36
+ def url_for(params)
37
+ ServicesPath + '?' + query_string(params)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0" encoding="utf-8" ?>
2
+ <rsp stat="ok">
3
+ <frob>934-746563215463214621</frob>
4
+ </rsp>
@@ -0,0 +1,5 @@
1
+ <auth>
2
+ <token>45-76598454353455</token>
3
+ <perms>read</perms>
4
+ <user nsid="12037949754@N01" username="Bees" fullname="Cal H" />
5
+ </auth>
@@ -0,0 +1,184 @@
1
+ $:.unshift File.join(File.dirname(__FILE__), '..')
2
+ require 'spec_helper'
3
+ require 'uri'
4
+
5
+ describe SimpleFlickr::Authentication do
6
+ before(:all) do
7
+ @options = {
8
+ :api_key => '1234',
9
+ :secret => 'tell no one',
10
+ :permission => :write
11
+ }
12
+ end
13
+
14
+ it "should not be possible to instantiate the base class" do
15
+ lambda {
16
+ SimpleFlickr::Authentication.new(@options)
17
+ }.should raise_error
18
+ end
19
+
20
+ it "should raise an error when not provided with an api key" do
21
+ lambda {
22
+ SimpleFlickr::DesktopAuthentication.new(@options.without(:api_key))
23
+ }.should raise_error(ArgumentError)
24
+ end
25
+
26
+ it "should raise an error when not provided with a secret" do
27
+ lambda {
28
+ SimpleFlickr::DesktopAuthentication.new(@options.without(:secret))
29
+ }.should raise_error(ArgumentError)
30
+ end
31
+
32
+ it "should not raise an error when provided with the right options" do
33
+ lambda {
34
+ SimpleFlickr::DesktopAuthentication.new(@options)
35
+ }.should_not raise_error
36
+ end
37
+
38
+ it "should default to read permission" do
39
+ @f = SimpleFlickr::DesktopAuthentication.new(@options.without(:permission))
40
+ @f.permission.should == :read
41
+ end
42
+ end
43
+
44
+ describe "Authentication subclasses" do
45
+
46
+ before(:all) do
47
+ @api_key = '9a0554259914a86fb9e7eb014e4e5d52'
48
+ @secret = '000005fab4534d05'
49
+ @options = { :api_key => @api_key, :secret => @secret, :permission => :write }
50
+ end
51
+
52
+ describe SimpleFlickr::WebAuthentication do
53
+
54
+ before(:all) do
55
+ @frob = '72157607642075605-dc0f15086c86023b-22944155'
56
+ @callback_url = "http://www.somehost.com/somedir?frob=#{@frob}"
57
+ end
58
+
59
+ before(:each) do
60
+ @f = SimpleFlickr::WebAuthentication.new(@options)
61
+ end
62
+
63
+ it "should generate a valid authentication url" do
64
+ uri = URI.parse @f.authentication_url
65
+ uri.host.should == 'www.flickr.com'
66
+ uri.path.should == '/services/auth/'
67
+
68
+ hsh = hash_from_query(uri.query)
69
+ hsh[:api_key].should == @api_key
70
+ hsh[:perms].should == 'write'
71
+ end
72
+
73
+ it "should return a frob from the return uri" do
74
+ @f.get_frob_from_url(@callback_url).should == @frob
75
+ end
76
+
77
+ it "should get a token when given an uri" do
78
+ @f.expects(:get_token_from_frob).with(@frob)
79
+ @f.get_token(@callback_url)
80
+ end
81
+
82
+ it "should get a token when given the frob" do
83
+ @f.expects(:get_token_from_frob).with(@frob)
84
+ @f.get_token(@frob)
85
+ end
86
+
87
+ end
88
+
89
+ describe SimpleFlickr::DesktopAuthentication do
90
+
91
+ before(:each) do
92
+ @f = SimpleFlickr::DesktopAuthentication.new({
93
+ :api_key => '9a0554259914a86fb9e7eb014e4e5d52',
94
+ :secret => '000005fab4534d05'
95
+ })
96
+ end
97
+
98
+ describe "authentication for desktop apps" do
99
+ # Key, secret and signature come from the Flickr API docs.
100
+ # http://www.flickr.com/services/api/auth.howto.desktop.html
101
+ it "should generate a correct url to retrieve a frob" do
102
+ path, query = @f.frob_url.split('?')
103
+ path.should == '/services/rest/'
104
+
105
+ hsh = hash_from_query(query)
106
+ hsh[:method].should == 'flickr.auth.getFrob'
107
+ hsh[:api_key].should == '9a0554259914a86fb9e7eb014e4e5d52'
108
+ hsh[:api_sig].should == '8ad70cd3888ce493c8dde4931f7d6bd0'
109
+ hsh[:secret].should be_nil
110
+ end
111
+
112
+ it "should generate an authentication url from a response with a frob" do
113
+ response = mock_response
114
+ response.stubs(:body).returns sample_xml('auth.getFrob')
115
+ @f.frob_from_response(response).should == '934-746563215463214621'
116
+ end
117
+
118
+ # The :api_sig parameter is documented wronly in the Flickr API docs. It says the string
119
+ # to sign ends in 'permswread', but this should be 'permsread' of course. Therefore
120
+ # the :api_sig here does not correspond to the Flickr docs, but it makes the spec valid.
121
+ it "should return an url to authenticate to containing a frob" do
122
+ response = mock_response
123
+ response.stubs(:body).returns sample_xml('auth.getFrob')
124
+ SimpleFlickr::HTTP.expects(:start).returns(response)
125
+ uri = URI.parse @f.authentication_url
126
+
127
+ uri.host.should == 'www.flickr.com'
128
+ uri.scheme.should == 'http'
129
+ uri.path.should == '/services/auth/'
130
+ hsh = hash_from_query(uri.query)
131
+ hsh[:api_key].should == '9a0554259914a86fb9e7eb014e4e5d52'
132
+ hsh[:api_sig].should == '0d08a9522d152d2e43daaa2a932edf67'
133
+ hsh[:frob].should == '934-746563215463214621'
134
+ hsh[:perms].should == 'read'
135
+ hsh[:secret].should be_nil
136
+ end
137
+
138
+ it "should throw an error when no frob is available" do
139
+ lambda do
140
+ @f.get_token
141
+ end.should raise_error
142
+ end
143
+
144
+ describe "with the frob available" do
145
+ before(:each) do
146
+ response = mock_response
147
+ response.stubs(:body).returns sample_xml('auth.getToken')
148
+ connection = mock('Connection')
149
+ connection.expects(:get).with do |value|
150
+ path, query = value.split('?')
151
+ path.should == '/services/rest/'
152
+
153
+ hsh = hash_from_query(query)
154
+ hsh[:method].should == 'flickr.auth.getToken'
155
+ hsh[:api_key].should == '9a0554259914a86fb9e7eb014e4e5d52'
156
+ hsh[:api_sig].should == 'a5902059792a7976d03be67bdb1e98fd'
157
+ hsh[:frob].should == '934-746563215463214621'
158
+ hsh[:secret].should be_nil
159
+ true
160
+ end
161
+ SimpleFlickr::HTTP.expects(:start).returns(response).yields(connection)
162
+ end
163
+
164
+ it "should get a token when frob is available" do
165
+ @f.expects(:frob).at_least_once.returns('934-746563215463214621')
166
+ @f.get_token.should == '45-76598454353455'
167
+ end
168
+
169
+ it "should get a token with the given frob" do
170
+ @f.get_token('934-746563215463214621').should == '45-76598454353455'
171
+ end
172
+ end
173
+ end
174
+ end
175
+
176
+ def hash_from_query(str)
177
+ str.split('&').inject({}) do |hsh, pair|
178
+ key, value = pair.split('=')
179
+ hsh[key.to_sym] = value
180
+ hsh
181
+ end
182
+ end
183
+
184
+ end
@@ -0,0 +1,146 @@
1
+ $:.unshift File.join(File.dirname(__FILE__), '..')
2
+ require 'spec_helper'
3
+
4
+ class TestClient < SimpleFlickr::Base
5
+ end
6
+
7
+ describe SimpleFlickr::Base, "defining api_key and secret" do
8
+
9
+ it "should declare #api_key and #secret as private" do
10
+ TestClient.private_methods.should include('api_key', 'secret')
11
+ end
12
+
13
+ it "should set an api key" do
14
+ TestClient.class_eval do
15
+ api_key 'my_key'
16
+ end
17
+ TestClient.class_eval{default_params}[:api_key].should == 'my_key'
18
+ end
19
+
20
+ it "should set a secret" do
21
+ TestClient.class_eval do
22
+ secret 'my_secret'
23
+ end
24
+ TestClient.class_eval{default_params}[:secret].should == 'my_secret'
25
+ end
26
+
27
+ end
28
+
29
+ describe SimpleFlickr::Base do
30
+ before(:all) do
31
+ TestClient.class_eval do
32
+ api_key 'my_key'
33
+ secret 'my_secret'
34
+ end
35
+ end
36
+
37
+ describe "authentication" do
38
+ it "should default to :read permission" do
39
+ web_auth = TestClient.web_authentication
40
+ web_auth.permission.should == :read
41
+ end
42
+
43
+ it "should accept setting the permission" do
44
+ web_auth = TestClient.web_authentication(:write)
45
+ web_auth.permission.should == :write
46
+ end
47
+
48
+ it "should throw an error when a non-valid permission was given" do
49
+ lambda do
50
+ TestClient.web_authentication(:rule_the_world)
51
+ end.should raise_error
52
+ end
53
+
54
+ it "should return a valid web authentication object" do
55
+ web_auth = TestClient.web_authentication
56
+ web_auth.should be_an_instance_of(SimpleFlickr::WebAuthentication)
57
+
58
+ hsh = web_auth.instance_eval { @default_params }
59
+ hsh[:api_key].should == 'my_key'
60
+ hsh[:secret].should == 'my_secret'
61
+ end
62
+
63
+ it "should return a valid desktop authentication object" do
64
+ desktop_auth = TestClient.desktop_authentication
65
+ desktop_auth.should be_an_instance_of(SimpleFlickr::DesktopAuthentication)
66
+
67
+ hsh = desktop_auth.instance_eval { @default_params }
68
+ hsh[:api_key].should == 'my_key'
69
+ hsh[:secret].should == 'my_secret'
70
+ end
71
+ end
72
+
73
+ describe "instantiation" do
74
+ it "should allow instantiation without arguments" do
75
+ t = TestClient.new
76
+ t.token.should be_nil
77
+ end
78
+
79
+ it "should accept a token for instantiation" do
80
+ t = TestClient.new('my_token')
81
+ t.token.should == 'my_token'
82
+ end
83
+ end
84
+
85
+ describe "token scoping" do
86
+ it "should be possible to call as class method" do
87
+ TestClient.with_token('token1') do
88
+ token.should == 'token1'
89
+ end
90
+ end
91
+
92
+ it "should be possible to call as instance method" do
93
+ TestClient.new.with_token('token1') do
94
+ token.should == 'token1'
95
+ end
96
+ end
97
+
98
+ it "should allow nesting of scopes" do
99
+ TestClient.with_token('token1') do
100
+ token.should == 'token1'
101
+ with_token 'token2' do
102
+ token.should == 'token2'
103
+ with_token 'token3' do
104
+ token.should == 'token3'
105
+ end
106
+ token.should == 'token2'
107
+ end
108
+ token.should == 'token1'
109
+ end
110
+ end
111
+ end
112
+
113
+ describe "doing a request" do
114
+ before(:each) do
115
+ @t = TestClient.new('some_token')
116
+ end
117
+
118
+ it "should throw an error when trying to define the same key twice" do
119
+ # To prevent redefining api_key or secret
120
+ lambda do
121
+ @t.get('some.action') do |p|
122
+ p.some_key = 'WIN'
123
+ p.some_key = 'FAIL'
124
+ end
125
+ end.should raise_error
126
+ end
127
+
128
+ it "should make an authenticated request" do
129
+ SimpleFlickr::Base::QueryBuilder.expects(:url_for).with do |hsh|
130
+ hsh[:api_key].should == 'my_key'
131
+ hsh[:secret].should == 'my_secret'
132
+ hsh[:method].should == 'flickr.photos.search'
133
+ hsh[:user_id].should == 'me'
134
+ hsh[:auth_token].should == 'some_token'
135
+ end.returns('my_generated_url')
136
+
137
+ connection = mock(:get => 'my_generated_url')
138
+ response = mock(:body => "<tag/>")
139
+ SimpleFlickr::HTTP.expects(:start).yields(connection).returns(response)
140
+
141
+ @t.get('photos.search') do |p|
142
+ p.user_id = :me
143
+ end.root.name.should == 'tag'
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,40 @@
1
+ $:.unshift File.join(File.dirname(__FILE__), '..')
2
+ require 'spec_helper'
3
+
4
+ class QueryTester
5
+ include SimpleFlickr::Query
6
+ end
7
+
8
+ describe SimpleFlickr::Query do
9
+ # The parameters used here come from:
10
+ # http://www.flickr.com/services/api/auth.howto.desktop.html
11
+
12
+ before(:all) do
13
+ @params = { :api_key => '9a0554259914a86fb9e7eb014e4e5d52', :method => 'flickr.auth.getFrob' }
14
+ @secret = '000005fab4534d05';
15
+ @signature_string = '000005fab4534d05api_key9a0554259914a86fb9e7eb014e4e5d52methodflickr.auth.getFrob'
16
+ @signature = '8ad70cd3888ce493c8dde4931f7d6bd0'
17
+ @query = QueryTester.new
18
+ end
19
+
20
+ it "should sort paramers by key" do
21
+ @query.sort_params({:b => '1', :c => '3', :a => '2'}).should == [[:a, '2'], [:b, '1'], [:c, '3']]
22
+ end
23
+
24
+ it "should return a valid string to sign" do
25
+ @query.string_to_sign(@params, @secret).should == @signature_string
26
+ end
27
+
28
+ it "should produce a valid signed string" do
29
+ @query.signature(@params, @secret).should == @signature
30
+ end
31
+
32
+ it "should produce a valid query part of an url" do
33
+ str = @query.query_string(@params.merge(:secret => @secret))
34
+ pairs = str.split('&')
35
+ pairs.size.should == 3
36
+ pairs.should include("api_key=#{@params[:api_key]}")
37
+ pairs.should include("method=#{@params[:method]}")
38
+ pairs.should include("api_sig=#{@signature}")
39
+ end
40
+ end
@@ -0,0 +1,2 @@
1
+ --exclude ^\/,^spec\/
2
+ --no-validator-links
@@ -0,0 +1,2 @@
1
+ --colour
2
+ --reverse
@@ -0,0 +1,93 @@
1
+ require 'rubygems'
2
+ # Include mocha before rspec because of some funky bug in
3
+ # rspec that depends on the constant Test being defined
4
+ gem 'mocha', '~> 0.9.0'
5
+ require 'mocha'
6
+ gem 'rspec', '~> 1.1.3'
7
+ require 'spec'
8
+
9
+ $:.unshift File.join File.dirname(__FILE__), %w{.. lib}
10
+ require 'simpleflickr'
11
+
12
+ module SampleFeeds
13
+ FEED_DIR = File.dirname(__FILE__) + '/feeds/'
14
+
15
+ def sample_xml(name)
16
+ File.read "#{FEED_DIR}#{name}.xml"
17
+ end
18
+ end
19
+
20
+ module HttpMocks
21
+ def mock_response(type = :success)
22
+ klass = case type
23
+ when :success then Net::HTTPSuccess
24
+ when :redirect then Net::HTTPRedirection
25
+ when :fail then Net::HTTPClientError
26
+ else type
27
+ end
28
+
29
+ klass.new(nil, nil, nil)
30
+ end
31
+
32
+ def mock_connection(ssl = true)
33
+ connection = mock('HTTP connection')
34
+ connection.stubs(:start)
35
+ connection.stubs(:finish)
36
+ if ssl
37
+ connection.expects(:use_ssl=).with(true)
38
+ connection.expects(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE)
39
+ end
40
+ connection
41
+ end
42
+ end
43
+
44
+ Spec::Runner.configure do |config|
45
+ config.include SampleFeeds, HttpMocks
46
+ # config.predicate_matchers[:swim] = :can_swim?
47
+
48
+ config.mock_with :mocha
49
+ end
50
+
51
+ module Mocha
52
+ module ParameterMatchers
53
+ def query_string(entries, partial = false)
54
+ QueryStringMatcher.new(entries, partial)
55
+ end
56
+ end
57
+ end
58
+
59
+ class QueryStringMatcher < Mocha::ParameterMatchers::Base
60
+
61
+ def initialize(entries, partial)
62
+ @entries = entries
63
+ @partial = partial
64
+ end
65
+
66
+ def matches?(available_parameters)
67
+ string = available_parameters.shift.split('?').last
68
+ broken = string.split('&').map { |pair| pair.split('=').map { |value| CGI.unescape(value) } }
69
+ hash = Hash[*broken.flatten]
70
+
71
+ if @partial
72
+ has_entry_matchers = @entries.map do |key, value|
73
+ Mocha::ParameterMatchers::HasEntry.new(key, value)
74
+ end
75
+ Mocha::ParameterMatchers::AllOf.new(*has_entry_matchers).matches?([hash])
76
+ else
77
+ @entries == hash
78
+ end
79
+ end
80
+
81
+ def mocha_inspect
82
+ "query_string(#{@entries.mocha_inspect})"
83
+ end
84
+
85
+ end
86
+
87
+ class Hash
88
+ def without(key)
89
+ r = self.clone
90
+ r.delete(key)
91
+ r
92
+ end
93
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pietern-simpleflickr
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.2
5
+ platform: ruby
6
+ authors:
7
+ - Pieter Noordhuis
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-01-26 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Rolling your own Flickr libs was never *this* easy
17
+ email: pcnoordhuis@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - History.txt
26
+ - Manifest.txt
27
+ - README.rdoc
28
+ - VERSION.yml
29
+ - lib/simpleflickr
30
+ - lib/simpleflickr/authentication
31
+ - lib/simpleflickr/authentication/authentication.rb
32
+ - lib/simpleflickr/authentication/desktop.rb
33
+ - lib/simpleflickr/authentication/web.rb
34
+ - lib/simpleflickr/base.rb
35
+ - lib/simpleflickr/http.rb
36
+ - lib/simpleflickr/query.rb
37
+ - lib/simpleflickr.rb
38
+ - spec/feeds
39
+ - spec/feeds/auth.getFrob.xml
40
+ - spec/feeds/auth.getToken.xml
41
+ - spec/flickr
42
+ - spec/flickr/auth_spec.rb
43
+ - spec/flickr/base_spec.rb
44
+ - spec/flickr/query_spec.rb
45
+ - spec/rcov.opts
46
+ - spec/spec.opts
47
+ - spec/spec_helper.rb
48
+ has_rdoc: true
49
+ homepage: http://github.com/pietern/simpleflickr
50
+ post_install_message:
51
+ rdoc_options:
52
+ - --inline-source
53
+ - --charset=UTF-8
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: "0"
61
+ version:
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: "0"
67
+ version:
68
+ requirements: []
69
+
70
+ rubyforge_project:
71
+ rubygems_version: 1.2.0
72
+ signing_key:
73
+ specification_version: 2
74
+ summary: Rolling your own Flickr libs was never *this* easy
75
+ test_files: []
76
+