pietern-simpleflickr 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +7 -0
- data/Manifest.txt +21 -0
- data/README.rdoc +140 -0
- data/VERSION.yml +4 -0
- data/lib/simpleflickr.rb +28 -0
- data/lib/simpleflickr/authentication/authentication.rb +38 -0
- data/lib/simpleflickr/authentication/desktop.rb +41 -0
- data/lib/simpleflickr/authentication/web.rb +22 -0
- data/lib/simpleflickr/base.rb +100 -0
- data/lib/simpleflickr/http.rb +36 -0
- data/lib/simpleflickr/query.rb +40 -0
- data/spec/feeds/auth.getFrob.xml +4 -0
- data/spec/feeds/auth.getToken.xml +5 -0
- data/spec/flickr/auth_spec.rb +184 -0
- data/spec/flickr/base_spec.rb +146 -0
- data/spec/flickr/query_spec.rb +40 -0
- data/spec/rcov.opts +2 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +93 -0
- metadata +76 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
@@ -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
|
data/README.rdoc
ADDED
@@ -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.
|
data/VERSION.yml
ADDED
data/lib/simpleflickr.rb
ADDED
@@ -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,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
|
data/spec/rcov.opts
ADDED
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -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
|
+
|