dropbox 0.0.10 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,160 @@
1
+ # Defines the Dropbox::Session class.
2
+
3
+ require 'oauth'
4
+
5
+ module Dropbox
6
+
7
+ # This class is a portal to the Dropbox API and a façade over the Ruby OAuth
8
+ # gem allowing developers to authenticate their user's Dropbox accounts.
9
+ #
10
+ # == Authenticating a user
11
+ #
12
+ # You start by creating a new instance and providing your OAuth consumer key
13
+ # and secret. You then call the authorize_url method on your new instance to
14
+ # receive the authorization URL.
15
+ #
16
+ # Once your user visits the URL, it will complete the authorization process on
17
+ # the server side. You should call the authorize method:
18
+ #
19
+ # session = Dropbox::Session.new(my_key, my_secret)
20
+ # puts "Now visit #{session.authorize_url}. Hit enter when you have completed authorization."
21
+ # gets
22
+ # session.authorize
23
+ #
24
+ # The authorize method must be called on the same instance of Dropbox::Session
25
+ # that gave you the URL. If this is unfeasible (for instance, you are doing
26
+ # this in a stateless Rails application), you can serialize the Session for
27
+ # storage (e.g., in your Rails session):
28
+ #
29
+ # def authorize
30
+ # if params[:oauth_token] then
31
+ # dropbox_session = Dropbox::Session.deserialize(session[:dropbox_session])
32
+ # dropbox_session.authorize(params)
33
+ # session[:dropbox_session] = dropbox_session.serialize # re-serialize the authenticated session
34
+ #
35
+ # redirect_to :action => 'upload'
36
+ # else
37
+ # dropbox_session = Dropbox::Session.new('your_consumer_key', 'your_consumer_secret')
38
+ # session[:dropbox_session] = dropbox_session.serialize
39
+ # redirect_to dropbox_session.authorize_url(:oauth_callback => root_url)
40
+ # end
41
+ # end
42
+ #
43
+ # == Working with the API
44
+ #
45
+ # This class includes the methods of the Dropbox::API module. See that module
46
+ # to learn how to continue using the API.
47
+
48
+ class Session
49
+ include API
50
+
51
+ # Begins the authorization process. Provide the OAuth key and secret of your
52
+ # API account, assigned by Dropbox. This is the first step in the
53
+ # authorization process.
54
+ #
55
+ # Options:
56
+ #
57
+ # +ssl+:: If true, uses SSL to connect to the Dropbox API server.
58
+
59
+ def initialize(oauth_key, oauth_secret, options={})
60
+ @ssl = options[:ssl].to_bool
61
+ @consumer = OAuth::Consumer.new(oauth_key, oauth_secret,
62
+ :site => (@ssl ? Dropbox::SSL_HOST : Dropbox::HOST),
63
+ :request_token_path => "/#{Dropbox::VERSION}/oauth/request_token",
64
+ :authorize_path => "/#{Dropbox::VERSION}/oauth/authorize",
65
+ :access_token_path => "/#{Dropbox::VERSION}/oauth/access_token")
66
+ @request_token = @consumer.get_request_token
67
+ end
68
+
69
+ # Returns a URL that is used to complete the authorization process. Visiting
70
+ # this URL is the second step in the authorization process, after creating
71
+ # the Session instance.
72
+
73
+ def authorize_url(*args)
74
+ if authorized? then
75
+ raise AlreadyAuthorizedError, "You have already been authorized; no need to get an authorization URL."
76
+ else
77
+ return @request_token.authorize_url(*args)
78
+ end
79
+ end
80
+
81
+ # Authorizes a user from the information returned by Dropbox. This is the
82
+ # third step in the authorization process, after sending the user to the
83
+ # authorize_url.
84
+ #
85
+ # You can pass to this method a hash containing the keys and values of the
86
+ # OAuth parameters returned by Dropbox. An example in Rails:
87
+ #
88
+ # session.authorize :oauth_verifier => params[:oauth_verifier]
89
+ #
90
+ # Returns a boolean indicating if authentication was successful.
91
+
92
+ def authorize(options={})
93
+ @access_token = @request_token.get_access_token(options)
94
+ @request_token = nil if @access_token
95
+ return @access_token.to_bool
96
+ end
97
+
98
+ # Returns true if this session has been authorized.
99
+
100
+ def authorized?
101
+ @access_token.to_bool
102
+ end
103
+
104
+ # Serializes this object into a string that can then be recreated with the
105
+ # Dropbox::Session.deserialize method.
106
+
107
+ def serialize
108
+ if authorized? then
109
+ [ @consumer.key, @consumer.secret, authorized?, @access_token.token, @access_token.secret ].to_yaml
110
+ else
111
+ [ @consumer.key, @consumer.secret, authorized?, @request_token.token, @request_token.secret ].to_yaml
112
+ end
113
+ end
114
+
115
+ # Deserializes an instance from a string created from the serialize method.
116
+ # Returns the recreated instance.
117
+
118
+ def self.deserialize(data)
119
+ consumer_key, consumer_secret, authorized, token, token_secret = YAML.load(StringIO.new(data))
120
+ raise ArgumentError, "Must provide a properly serialized #{self.to_s} instance" unless [ consumer_key, consumer_secret, token, token_secret ].all? and authorized == true or authorized == false
121
+
122
+ session = self.new(consumer_key, consumer_secret)
123
+ if authorized then
124
+ session.instance_variable_set :@access_token, OAuth::AccessToken.new(session.instance_variable_get(:@consumer), token, token_secret)
125
+ else
126
+ session.instance_variable_set :@request_token, OAuth::RequestToken.new(session.instance_variable_get(:@consumer), token, token_secret)
127
+ end
128
+
129
+ return session
130
+ end
131
+
132
+ def inspect # :nodoc:
133
+ "#<#{self.class.to_s} #{@consumer.key} (#{'un' unless authorized?}authorized)>"
134
+ end
135
+
136
+ private
137
+
138
+ def access_token
139
+ @access_token || raise(UnauthorizedError, "You need to authorize the Dropbox user before you can call API methods")
140
+ end
141
+
142
+ def clone_with_host(host)
143
+ session = dup
144
+ consumer = OAuth::Consumer.new(@consumer.key, @consumer.secret, :site => host)
145
+ session.instance_variable_set :@consumer, consumer
146
+ session.instance_variable_set :@access_token, OAuth::AccessToken.new(consumer, @access_token.token, @access_token.secret)
147
+ return session
148
+ end
149
+ end
150
+
151
+ # Raised when trying to call Dropbox API methods without yet having completed
152
+ # the OAuth process.
153
+
154
+ class UnauthorizedError < StandardError; end
155
+
156
+ # Raised when trying to call Dropbox::Session#authorize_url on an already
157
+ # authorized session.
158
+
159
+ class AlreadyAuthorizedError < StandardError; end
160
+ end
@@ -0,0 +1,9 @@
1
+ class Array # :nodoc:
2
+ def extract_options! # :nodoc:
3
+ last.is_a?(::Hash) ? pop : {}
4
+ end unless method_defined?(:extract_options!)
5
+
6
+ def to_hash # :nodoc:
7
+ inject({}) { |hsh, (k,v)| hsh[k] = v ; hsh }
8
+ end unless method_defined?(:to_hash)
9
+ end
@@ -0,0 +1,61 @@
1
+ class Hash # :nodoc:
2
+ def slice(*keys) #:nodoc:
3
+ keys = keys.map! { |key| convert_key(key) } if respond_to?(:convert_key)
4
+ hash = self.class.new
5
+ keys.each { |k| hash[k] = self[k] if has_key?(k) }
6
+ hash
7
+ end unless method_defined?(:slice)
8
+
9
+ def symbolize_keys # :nodoc:
10
+ inject({}) do |options, (key, value)|
11
+ options[(key.to_sym rescue key) || key] = value
12
+ options
13
+ end
14
+ end unless method_defined?(:symbolize_keys)
15
+
16
+ def symbolize_keys! # :nodoc:
17
+ self.replace(self.symbolize_keys)
18
+ end unless method_defined?(:symbolize_keys!)
19
+
20
+ def symbolize_keys_recursively # :nodoc:
21
+ hsh = symbolize_keys
22
+ hsh.each { |k, v| hsh[k] = v.symbolize_keys_recursively if v.kind_of?(Hash) }
23
+ hsh.each { |k, v| hsh[k] = v.map { |i| i.kind_of?(Hash) ? i.symbolize_keys_recursively : i } if v.kind_of?(Array) }
24
+ return hsh
25
+ end
26
+
27
+ def stringify_keys # :nodoc:
28
+ inject({}) do |options, (key, value)|
29
+ options[(key.to_s rescue key) || key] = value
30
+ options
31
+ end
32
+ end unless method_defined?(:stringify_keys)
33
+
34
+ def stringify_keys! # :nodoc:
35
+ self.replace(self.stringify_keys)
36
+ end unless method_defined?(:stringify_keys!)
37
+
38
+ def stringify_keys_recursively # :nodoc:
39
+ hsh = stringify_keys
40
+ hsh.each { |k, v| hsh[k] = v.stringify_keys_recursively if v.kind_of?(Hash) }
41
+ hsh.each { |k, v| hsh[k] = v.map { |i| i.kind_of?(Hash) ? i.stringify_keys_recursively : i } if v.kind_of?(Array) }
42
+ return hsh
43
+ end
44
+
45
+ def to_struct # :nodoc:
46
+ struct = Struct.new(*keys).new(*values)
47
+ # attach methods for any predicate keys, since Struct.new doesn't seem to do that
48
+ pred_keys = slice(*(keys.select { |key| key.to_s.ends_with?('?') }))
49
+ pred_keys.each do |key, val|
50
+ struct.eigenclass.send(:define_method, key.to_sym) { return val }
51
+ end
52
+ return struct
53
+ end
54
+
55
+ def to_struct_recursively # :nodoc:
56
+ hsh = dup
57
+ hsh.each { |k, v| hsh[k] = v.to_struct_recursively if v.kind_of?(Hash) }
58
+ hsh.each { |k, v| hsh[k] = v.map { |i| i.kind_of?(Hash) ? i.to_struct_recursively : i } if v.kind_of?(Array) }
59
+ return hsh.to_struct
60
+ end
61
+ end
@@ -0,0 +1,22 @@
1
+ class Module # :nodoc:
2
+ def alias_method_chain(target, feature) # :nodoc:
3
+ # Strip out punctuation on predicates or bang methods since
4
+ # e.g. target?_without_feature is not a valid method name.
5
+ aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1
6
+ yield(aliased_target, punctuation) if block_given?
7
+
8
+ with_method, without_method = "#{aliased_target}_with_#{feature}#{punctuation}", "#{aliased_target}_without_#{feature}#{punctuation}"
9
+
10
+ alias_method without_method, target
11
+ alias_method target, with_method
12
+
13
+ case
14
+ when public_method_defined?(without_method)
15
+ public target
16
+ when protected_method_defined?(without_method)
17
+ protected target
18
+ when private_method_defined?(without_method)
19
+ private target
20
+ end
21
+ end unless method_defined?(:alias_method_chain)
22
+ end
@@ -0,0 +1,5 @@
1
+ class Object # :nodoc:
2
+ def eigenclass # :nodoc:
3
+ (class << self; self; end)
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ class String # :nodoc:
2
+ def starts_with?(prefix) # :nodoc:
3
+ self[0, prefix.length] == prefix
4
+ end unless method_defined?(:starts_with?)
5
+
6
+ def ends_with?(suffix) # :nodoc:
7
+ self[-suffix.length, suffix.length] == suffix
8
+ end unless method_defined?(:ends_with?)
9
+ end
@@ -0,0 +1,17 @@
1
+ class Object # :nodoc:
2
+ def to_bool # :nodoc:
3
+ true
4
+ end
5
+ end
6
+
7
+ class FalseClass # :nodoc:
8
+ def to_bool # :nodoc:
9
+ false
10
+ end
11
+ end
12
+
13
+ class NilClass # :nodoc:
14
+ def to_bool # :nodoc:
15
+ false
16
+ end
17
+ end
@@ -0,0 +1,778 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ def url_args(url)
4
+ return {} unless url.include?('?')
5
+ _, back = url.split('?')
6
+ return {} unless back
7
+ back.split('&').map { |comp| comp.split('=') }.to_hash
8
+ end
9
+
10
+ def should_receive_api_method_with_arguments(object, method, api_method, arguments, response, path=nil, root=nil)
11
+ object.should_receive(method).once do |url|
12
+ front = url.split('?').first
13
+ front.should eql("#{Dropbox::ALTERNATE_HOSTS[api_method] || Dropbox::HOST}/#{Dropbox::VERSION}/#{api_method}#{'/' + root if root}#{'/' + path if path}")
14
+
15
+ query_params = url_args(url)
16
+ query_params.each { |key, val| val.should eql(arguments[key.to_sym]) }
17
+ arguments.each { |key, _| query_params.should include(key.to_s) }
18
+ response
19
+ end
20
+ end
21
+
22
+ def stub_for_upload_testing
23
+ @consumer_mock.stub!(:key).and_return("consumer key")
24
+ @consumer_mock.stub!(:secret).and_return("consumer secret")
25
+ @consumer_mock.stub!(:sign!).and_return { |req, _| req.stub!(:to_hash).and_return('authorization' => ["Oauth", "test"]) }
26
+
27
+ @token_mock.stub!(:token).and_return("access token")
28
+ @token_mock.stub!(:secret).and_return("access secret")
29
+
30
+ @response.stub!(:kind_of?).with(Net::HTTPSuccess).and_return(true)
31
+ @response.stub!(:body).and_return('{"test":"val"}')
32
+
33
+ Net::HTTP.stub!(:start).and_return(@response)
34
+ end
35
+
36
+ def response_acts_as(subclass)
37
+ @response.stub(:kind_of?).and_return(false)
38
+ @response.stub(:kind_of?).with(subclass).and_return(true) if subclass
39
+ end
40
+
41
+ describe Dropbox::API do
42
+ before :each do
43
+ @consumer_mock = mock("OAuth::Consumer")
44
+ token_mock = mock("OAuth::RequestToken")
45
+ @token_mock = mock("OAuth::AccessToken")
46
+ token_mock.stub!(:get_access_token).and_return(@token_mock)
47
+ @consumer_mock.stub!(:get_request_token).and_return(token_mock)
48
+ OAuth::Consumer.stub!(:new).and_return(@consumer_mock)
49
+
50
+ @session = Dropbox::Session.new('foo', 'bar')
51
+ @session.authorize
52
+
53
+ @response = mock('Net::HTTPResponse')
54
+ @response.stub!(:kind_of?).with(Net::HTTPSuccess).and_return(true)
55
+ @response.stub!(:code).and_return(200)
56
+ @response.stub!(:body).and_return("response body")
57
+ end
58
+
59
+ describe "#account" do
60
+ it "should call the /account/info API method" do
61
+ @response.stub!(:body).and_return('{"a":"b"}')
62
+ should_receive_api_method_with_arguments @token_mock, :get, 'account/info', {}, @response
63
+ @session.account
64
+ end
65
+
66
+ it "should convert the result into a struct" do
67
+ @response.stub!(:body).and_return( { :foo => :bar, :baz => { :hey => :you } }.to_json)
68
+ @token_mock.stub!(:get).and_return(@response)
69
+ result = @session.account
70
+ result.foo.should eql('bar')
71
+ result.baz.hey.should eql('you')
72
+ end
73
+ end
74
+
75
+ describe "#download" do
76
+ it "should call the files API method" do
77
+ should_receive_api_method_with_arguments @token_mock, :get, 'files', {}, @response, 'path/to/file', 'sandbox'
78
+ @session.download "path/to/file"
79
+ end
80
+
81
+ it "should strip a leading slash" do
82
+ should_receive_api_method_with_arguments @token_mock, :get, 'files', {}, @response, 'path/to/file', 'sandbox'
83
+ @session.download "/path/to/file"
84
+ end
85
+
86
+ it "should return the body of the response" do
87
+ @token_mock.stub!(:get).and_return(@response)
88
+ @session.download("path/to/file").should eql("response body")
89
+ end
90
+
91
+ it "should check the path" do
92
+ path = "test/path"
93
+ Dropbox.should_receive(:check_path).once.with(path).and_return(path)
94
+ @token_mock.stub!(:get).and_return(@response)
95
+
96
+ @session.download(path)
97
+ end
98
+ end
99
+
100
+ describe "#copy" do
101
+ before :each do
102
+ @response.stub!(:body).and_return('{"a":"b"}')
103
+ end
104
+
105
+ it "should call the fileops/copy API method" do
106
+ should_receive_api_method_with_arguments @token_mock, :post, 'fileops/copy', { :from_path => 'source%2Ffile', :to_path => 'dest%2Ffile', :root => 'sandbox' }, @response
107
+ @session.copy 'source/file', 'dest/file'
108
+ end
109
+
110
+ it "should return the metadata as a struct" do
111
+ @response.stub!(:body).and_return( { :foo => :bar, :baz => { :hey => :you } }.to_json)
112
+ @token_mock.stub!(:post).and_return(@response)
113
+
114
+ result = @session.copy('a', 'b')
115
+ result.foo.should eql('bar')
116
+ result.baz.hey.should eql('you')
117
+ end
118
+
119
+ it "should strip a leading slash from source" do
120
+ should_receive_api_method_with_arguments @token_mock, :post, 'fileops/copy', { :from_path => 'source%2Ffile', :to_path => 'dest%2Ffile', :root => 'sandbox' }, @response
121
+ @session.copy '/source/file', 'dest/file'
122
+ end
123
+
124
+ it "should strip a leading slash from target" do
125
+ should_receive_api_method_with_arguments @token_mock, :post, 'fileops/copy', { :from_path => 'source%2Ffile', :to_path => 'dest%2Ffile', :root => 'sandbox' }, @response
126
+ @session.copy 'source/file', '/dest/file'
127
+ end
128
+
129
+ it "should set the target file name to the source file name if the target is a directory path" do
130
+ should_receive_api_method_with_arguments @token_mock, :post, 'fileops/copy', { :from_path => 'source%2Ffile', :to_path => 'dest%2Ffile', :root => 'sandbox' }, @response
131
+ @session.copy 'source/file', 'dest/'
132
+ end
133
+
134
+ it "should re-raise 404's as FileNotFoundErrors" do
135
+ response_acts_as Net::HTTPNotFound
136
+ @token_mock.stub!(:post).and_return(@response)
137
+
138
+ lambda { @session.copy('a', 'b') }.should raise_error(Dropbox::FileNotFoundError)
139
+ end
140
+
141
+ it "should re-raise 403's as FileExistsErrors" do
142
+ response_acts_as Net::HTTPForbidden
143
+ @token_mock.stub!(:post).and_return(@response)
144
+
145
+ lambda { @session.copy('a', 'b') }.should raise_error(Dropbox::FileExistsError)
146
+ end
147
+
148
+ it "should raise other errors unmodified" do
149
+ @response.stub(:kind_of?).and_return(false)
150
+ @token_mock.stub!(:post).and_return(@response)
151
+
152
+ lambda { @session.copy('a', 'b') }.should raise_error(Dropbox::UnsuccessfulResponseError)
153
+ end
154
+
155
+ it "should check the source and destination paths" do
156
+ source_path = "source/path"
157
+ dest_path = "dest/path"
158
+ Dropbox.should_receive(:check_path).once.with(source_path).and_return(source_path)
159
+ Dropbox.should_receive(:check_path).once.with(dest_path).and_return(dest_path)
160
+ @token_mock.stub!(:post).and_return(@response)
161
+
162
+ @session.copy(source_path, dest_path)
163
+ end
164
+ end
165
+
166
+ describe "#move" do
167
+ before :each do
168
+ @response.stub!(:body).and_return('{"a":"b"}')
169
+ end
170
+
171
+ it "should call the fileops/move API method" do
172
+ should_receive_api_method_with_arguments @token_mock, :post, 'fileops/move', { :from_path => 'source%2Ffile', :to_path => 'dest%2Ffile', :root => 'sandbox' }, @response
173
+ @session.move 'source/file', 'dest/file'
174
+ end
175
+
176
+ it "should return the metadata as a struct" do
177
+ @response.stub!(:body).and_return( { :foo => :bar, :baz => { :hey => :you } }.to_json)
178
+ @token_mock.stub!(:post).and_return(@response)
179
+
180
+ result = @session.move('a', 'b')
181
+ result.foo.should eql('bar')
182
+ result.baz.hey.should eql('you')
183
+ end
184
+
185
+ it "should strip a leading slash from source" do
186
+ should_receive_api_method_with_arguments @token_mock, :post, 'fileops/move', { :from_path => 'source%2Ffile', :to_path => 'dest%2Ffile', :root => 'sandbox' }, @response
187
+ @session.move '/source/file', 'dest/file'
188
+ end
189
+
190
+ it "should strip a leading slash from target" do
191
+ should_receive_api_method_with_arguments @token_mock, :post, 'fileops/move', { :from_path => 'source%2Ffile', :to_path => 'dest%2Ffile', :root => 'sandbox' }, @response
192
+ @session.move 'source/file', '/dest/file'
193
+ end
194
+
195
+ it "should set the target file name to the source file name if the target is a directory path" do
196
+ should_receive_api_method_with_arguments @token_mock, :post, 'fileops/move', { :from_path => 'source%2Ffile', :to_path => 'dest%2Ffile', :root => 'sandbox' }, @response
197
+ @session.move 'source/file', 'dest/'
198
+ end
199
+
200
+ it "should re-raise 404's as FileNotFoundErrors" do
201
+ response_acts_as Net::HTTPNotFound
202
+ @token_mock.stub!(:post).and_return(@response)
203
+
204
+ lambda { @session.move('a', 'b') }.should raise_error(Dropbox::FileNotFoundError)
205
+ end
206
+
207
+ it "should re-raise 403's as FileExistsErrors" do
208
+ response_acts_as Net::HTTPForbidden
209
+ @token_mock.stub!(:post).and_return(@response)
210
+
211
+ lambda { @session.move('a', 'b') }.should raise_error(Dropbox::FileExistsError)
212
+ end
213
+
214
+ it "should raise other errors unmodified" do
215
+ response_acts_as nil
216
+ @token_mock.stub!(:post).and_return(@response)
217
+
218
+ lambda { @session.move('a', 'b') }.should raise_error(Dropbox::UnsuccessfulResponseError)
219
+ end
220
+
221
+ it "should check the source and destination paths" do
222
+ source_path = "source/path"
223
+ dest_path = "dest/path"
224
+ Dropbox.should_receive(:check_path).once.with(source_path).and_return(source_path)
225
+ Dropbox.should_receive(:check_path).once.with(dest_path).and_return(dest_path)
226
+ @token_mock.stub!(:post).and_return(@response)
227
+
228
+ @session.move(source_path, dest_path)
229
+ end
230
+ end
231
+
232
+ describe "#rename" do
233
+ it "should raise an error if the new name has a slash in it" do
234
+ lambda { @session.rename 'file', 'new/name' }.should raise_error(ArgumentError)
235
+ end
236
+
237
+ it "should call move with the appropriate path and return the result of the call" do
238
+ @session.should_receive(:move).once.with('old/path/to/file', 'old/path/to/new_file', :mode => :sandbox).and_return(@response)
239
+ @session.rename('old/path/to/file', 'new_file', :mode => :sandbox).should eql(@response)
240
+ end
241
+ end
242
+
243
+ describe "#create_folder" do
244
+ before :each do
245
+ @response.stub!(:body).and_return('{"a":"b"}')
246
+ end
247
+
248
+ it "should call the fileops/create_folder API method" do
249
+ should_receive_api_method_with_arguments @token_mock, :post, 'fileops/create_folder', { :path => 'new%2Ffolder', :root => 'sandbox' }, @response
250
+ @session.create_folder 'new/folder'
251
+ end
252
+
253
+ it "should return the metadata as a struct" do
254
+ @response.stub!(:body).and_return( { :foo => :bar, :baz => { :hey => :you } }.to_json)
255
+ @token_mock.stub!(:post).and_return(@response)
256
+
257
+ result = @session.create_folder('a')
258
+ result.foo.should eql('bar')
259
+ result.baz.hey.should eql('you')
260
+ end
261
+
262
+ it "should strip a leading slash from the path" do
263
+ should_receive_api_method_with_arguments @token_mock, :post, 'fileops/create_folder', { :path => 'new%2Ffolder', :root => 'sandbox' }, @response
264
+ @session.create_folder '/new/folder'
265
+ end
266
+
267
+ it "should strip a trailing slash from the path" do
268
+ should_receive_api_method_with_arguments @token_mock, :post, 'fileops/create_folder', { :path => 'new%2Ffolder', :root => 'sandbox' }, @response
269
+ @session.create_folder 'new/folder/'
270
+ end
271
+
272
+ it "should re-raise 403's as FileExistsErrors" do
273
+ response_acts_as Net::HTTPForbidden
274
+ @token_mock.stub!(:post).and_return(@response)
275
+
276
+ lambda { @session.create_folder('a') }.should raise_error(Dropbox::FileExistsError)
277
+ end
278
+
279
+ it "should raise other errors unmodified" do
280
+ response_acts_as nil
281
+ @token_mock.stub!(:post).and_return(@response)
282
+
283
+ lambda { @session.create_folder('a') }.should raise_error(Dropbox::UnsuccessfulResponseError)
284
+ end
285
+
286
+ it "should check the path" do
287
+ path = "source/path"
288
+ Dropbox.should_receive(:check_path).once.with(path).and_return(path)
289
+ @token_mock.stub!(:post).and_return(@response)
290
+
291
+ @session.create_folder(path)
292
+ end
293
+ end
294
+
295
+ describe "#delete" do
296
+ it "should call the API method fileops/delete" do
297
+ should_receive_api_method_with_arguments @token_mock, :post, 'fileops/delete', { :path => 'some%2Ffile', :root => 'sandbox' }, @response
298
+ @session.delete 'some/file'
299
+ end
300
+
301
+ it "should return true" do
302
+ @token_mock.stub!(:post).and_return(@response)
303
+ @session.delete('some/file').should be_true
304
+ end
305
+
306
+ it "should strip a leading slash from the path" do
307
+ should_receive_api_method_with_arguments @token_mock, :post, 'fileops/delete', { :path => 'some%2Ffile', :root => 'sandbox' }, @response
308
+ @session.delete '/some/file'
309
+ end
310
+
311
+ it "should strip a trailing slash from the path" do
312
+ should_receive_api_method_with_arguments @token_mock, :post, 'fileops/delete', { :path => 'some%2Ffile', :root => 'sandbox' }, @response
313
+ @session.delete 'some/file/'
314
+ end
315
+
316
+ it "should re-raise 404's as FileNotFoundErrors" do
317
+ response_acts_as Net::HTTPNotFound
318
+ @token_mock.stub!(:post).and_return(@response)
319
+
320
+ lambda { @session.delete('a') }.should raise_error(Dropbox::FileNotFoundError)
321
+ end
322
+
323
+ it "should raise other errors unmodified" do
324
+ response_acts_as nil
325
+ @token_mock.stub!(:post).and_return(@response)
326
+
327
+ lambda { @session.delete('a') }.should raise_error(Dropbox::UnsuccessfulResponseError)
328
+ end
329
+
330
+ it "should check the path" do
331
+ path = "source/path"
332
+ Dropbox.should_receive(:check_path).once.with(path).and_return(path)
333
+ @token_mock.stub!(:post).and_return(@response)
334
+
335
+ @session.delete(path)
336
+ end
337
+ end
338
+
339
+ describe "#link" do
340
+ before :each do
341
+ @response.stub!(:code).and_return(304)
342
+ response_acts_as Net::HTTPFound
343
+ @response.stub!(:[]).and_return("new location")
344
+ end
345
+
346
+ it "should call the API method links" do
347
+ should_receive_api_method_with_arguments @token_mock, :get, 'links', {}, @response, 'some/file', 'sandbox'
348
+ @session.link 'some/file'
349
+ end
350
+
351
+ it "should strip a leading slash" do
352
+ should_receive_api_method_with_arguments @token_mock, :get, 'links', {}, @response, 'some/file', 'sandbox'
353
+ @session.link '/some/file'
354
+ end
355
+
356
+ it "should rescue 304's and return the Location header" do
357
+ should_receive_api_method_with_arguments @token_mock, :get, 'links', {}, @response, 'some/file', 'sandbox'
358
+ lambda { @session.link('some/file').should eql("new location") }.should_not raise_error
359
+ end
360
+
361
+ it "should re-raise other errors unmodified" do
362
+ response_acts_as nil
363
+ @token_mock.stub!(:get).and_return(@response)
364
+ lambda { @session.link('a') }.should raise_error(Dropbox::UnsuccessfulResponseError)
365
+ end
366
+
367
+ it "should check the path" do
368
+ path = "source/path"
369
+ Dropbox.should_receive(:check_path).once.with(path).and_return(path)
370
+ @token_mock.stub!(:get).and_return(@response)
371
+
372
+ @session.link(path)
373
+ end
374
+ end
375
+
376
+ describe "#metadata" do
377
+ before :each do
378
+ @response.stub!(:body).and_return('{"a":"b"}')
379
+ end
380
+
381
+ it "should call the API method metadata" do
382
+ should_receive_api_method_with_arguments @token_mock, :get, 'metadata', { :list => 'true' }, @response, 'some/file', 'sandbox'
383
+ @session.metadata 'some/file'
384
+ end
385
+
386
+ it "should strip a leading slash" do
387
+ should_receive_api_method_with_arguments @token_mock, :get, 'metadata', { :list => 'true' }, @response, 'some/file', 'sandbox'
388
+ @session.metadata '/some/file'
389
+ end
390
+
391
+ it "should set file_limit if :limit is set" do
392
+ should_receive_api_method_with_arguments @token_mock, :get, 'metadata', { :list => 'true', :file_limit => '123' }, @response, 'some/file', 'sandbox'
393
+ @session.metadata 'some/file', :limit => 123
394
+ end
395
+
396
+ it "should set list=false if :suppress_list is set" do
397
+ should_receive_api_method_with_arguments @token_mock, :get, 'metadata', { :list => 'false' }, @response, 'some/file', 'sandbox'
398
+ @session.metadata 'some/file', :suppress_list => true
399
+ end
400
+
401
+ it "should rescue 406's and re-raise them as TooManyEntriesErrors" do
402
+ response_acts_as Net::HTTPNotAcceptable
403
+ @token_mock.stub!(:get).and_return(@response)
404
+
405
+ lambda { @session.metadata('a') }.should raise_error(Dropbox::TooManyEntriesError)
406
+ end
407
+
408
+ it "should rescue 404's and re-raise them as FileNotFoundErrors" do
409
+ response_acts_as Net::HTTPNotFound
410
+ @token_mock.stub!(:get).and_return(@response)
411
+
412
+ lambda { @session.metadata('a') }.should raise_error(Dropbox::FileNotFoundError)
413
+ end
414
+
415
+ it "should re-raise other errors unmodified" do
416
+ response_acts_as nil
417
+ @token_mock.stub!(:get).and_return(@response)
418
+
419
+ lambda { @session.metadata('a') }.should raise_error(Dropbox::UnsuccessfulResponseError)
420
+ end
421
+
422
+ it "should check the path" do
423
+ path = "source/path"
424
+ Dropbox.should_receive(:check_path).once.with(path).and_return(path)
425
+ @token_mock.stub!(:get).and_return(@response)
426
+
427
+ @session.metadata(path)
428
+ end
429
+
430
+ it "should recursively convert the modification date into a Time" do
431
+ time = Time.now
432
+ @response.stub!(:body).and_return({ :modified => time.to_s, :hsh => { :modified => time.to_s } }.to_json)
433
+ @token_mock.stub!(:get).and_return(@response)
434
+
435
+ response = @session.metadata('path')
436
+
437
+ response.modified.should be_kind_of(Time)
438
+ response.modified.to_i.should == time.to_i
439
+ response.hsh.modified.should be_kind_of(Time)
440
+ response.hsh.modified.to_i.should == time.to_i
441
+ end
442
+
443
+ it "should add a directory? item recursively" do
444
+ @response.stub!(:body).and_return({ :is_dir => true, :hsh => { :is_dir => false } }.to_json)
445
+ @token_mock.stub!(:get).and_return(@response)
446
+
447
+ response = @session.metadata('path')
448
+
449
+ response.directory?.should be_true
450
+ response.hsh.directory?.should be_false
451
+ end
452
+ end
453
+
454
+ describe "#list" do
455
+ before :each do
456
+ @response = mock('metadata')
457
+ end
458
+
459
+ it "should call the metadata method and return the contents attribute" do
460
+ @response.should_receive(:contents).once.and_return([ 'contents' ])
461
+ @session.should_receive(:metadata).once.with('my/file', an_instance_of(Hash)).and_return(@response)
462
+
463
+ @session.list('my/file').should == [ 'contents' ]
464
+ end
465
+
466
+ it "should not allow suppress_list to be set to true" do
467
+ @response.stub!(:contents)
468
+ @session.should_receive(:metadata).once.with('my/file', hash_including(:hash => true, :suppress_list => false)).and_return(@response)
469
+
470
+ @session.list('my/file', :suppress_list => true, :hash => true)
471
+ end
472
+ end
473
+
474
+ describe "#upload" do
475
+ before :each do
476
+ stub_for_upload_testing
477
+ end
478
+
479
+ it "should check the path" do
480
+ path = "dest/path"
481
+ Dropbox.should_receive(:check_path).once.with(path).and_return(path)
482
+ @session.upload(__FILE__, path)
483
+ end
484
+
485
+ describe "parameters" do
486
+ describe "given a File object" do
487
+ before :each do
488
+ @file = File.open(__FILE__)
489
+ end
490
+
491
+ after :each do
492
+ @file.close
493
+ end
494
+
495
+ it "should use the File object as the stream" do
496
+ UploadIO.should_receive(:convert!).once.with(@file, anything, File.basename(__FILE__), __FILE__)
497
+ @session.upload @file, 'remote/'
498
+ end
499
+ end
500
+
501
+ describe "given a String object" do
502
+ before :each do
503
+ @string = __FILE__
504
+ @file = File.new(__FILE__)
505
+ File.should_receive(:new).once.with(@string).and_return(@file)
506
+ end
507
+
508
+ it "should use the file at that path as the stream" do
509
+ UploadIO.should_receive(:convert!).once.with(@file, anything, File.basename(__FILE__), __FILE__)
510
+ @session.upload @string, 'remote/'
511
+ end
512
+ end
513
+
514
+ it "should raise an error if given an unknown argument type" do
515
+ lambda { @session.upload 123, 'path' }.should raise_error(ArgumentError)
516
+ end
517
+ end
518
+
519
+ describe "request" do
520
+ before :each do
521
+ @request = mock('Net::HTTPRequest')
522
+ @request.stub!(:[]=)
523
+ end
524
+
525
+ it "should strip a leading slash from the remote path" do
526
+ Net::HTTP::Post::Multipart.should_receive(:new).once do |*args|
527
+ args.first.should eql("/#{Dropbox::VERSION}/files/sandbox/path")
528
+ @request
529
+ end
530
+
531
+ @session.upload __FILE__, '/path'
532
+ end
533
+
534
+ it "should call the files API method" do
535
+ Net::HTTP::Post::Multipart.should_receive(:new).once do |*args|
536
+ args.first.should eql("/#{Dropbox::VERSION}/files/sandbox/path/to/file")
537
+ @request
538
+ end
539
+
540
+ @session.upload __FILE__, 'path/to/file'
541
+ end
542
+
543
+ it "should use the sandbox root if specified" do
544
+ Net::HTTP::Post::Multipart.should_receive(:new).once do |*args|
545
+ args.first.should eql("/#{Dropbox::VERSION}/files/sandbox/path/to/file")
546
+ @request
547
+ end
548
+
549
+ @session.upload __FILE__, 'path/to/file', :mode => :sandbox
550
+ end
551
+
552
+ it "should set the authorization content header to the signed OAuth request" do
553
+ Net::HTTP::Post::Multipart.stub!(:new).and_return(@request)
554
+ @request.should_receive(:[]=).once.with('authorization', 'Oauth, test')
555
+
556
+ @session.upload __FILE__, 'blah'
557
+ end
558
+
559
+ it "should create a multipart POST request with the 'file' parameter set to the file of type application/octet-stream" do
560
+ Net::HTTP::Post::Multipart.should_receive(:new).once.with("/#{Dropbox::VERSION}/files/sandbox/hello", hash_including('file' => an_instance_of(File))).and_return(@request)
561
+
562
+ @session.upload __FILE__, 'hello'
563
+ end
564
+
565
+ it "should send the request" do
566
+ uri = URI.parse(Dropbox::ALTERNATE_HOSTS['files'])
567
+ Net::HTTP.should_receive(:start).once.with(uri.host, uri.port).and_return(@response)
568
+
569
+ @session.upload __FILE__, 'test'
570
+ end
571
+
572
+ it "should send an SSL request" do
573
+ @session = Dropbox::Session.new('foo', 'bar', :ssl => true)
574
+ @session.authorize
575
+
576
+ uri = URI.parse(Dropbox::ALTERNATE_SSL_HOSTS['files'])
577
+ Net::HTTP.should_receive(:start).once.with(uri.host, uri.port).and_return(@response)
578
+
579
+ @session.upload __FILE__, 'test'
580
+ end
581
+ end
582
+ end
583
+
584
+ describe "#event_metadata" do
585
+ it "should call the API method event_metadata" do
586
+ @response.stub!(:body).and_return('{"a":"b"}')
587
+ should_receive_api_method_with_arguments @token_mock, :get, 'event_metadata', { :root => 'sandbox', :target_events => 'event_json' }, @response
588
+ @session.event_metadata 'event_json'
589
+ end
590
+
591
+ it "should return the JSON-parsed response" do
592
+ resp = { :some => { :json => 123 } }
593
+ @response.stub!(:body).and_return(resp.to_json)
594
+ @token_mock.stub!(:get).and_return(@response)
595
+ @session.event_metadata('event_json').should == resp
596
+ end
597
+ end
598
+
599
+ describe "#event_content" do
600
+ it "should call the API method event_content" do
601
+ @response.stub!(:body).and_return('content')
602
+ @response.stub!(:header).and_return('X-Dropbox-Metadata' => '{"a":"b"}')
603
+ should_receive_api_method_with_arguments @token_mock, :get, 'event_content', { :root => 'sandbox', :target_event => '1%3A2%3A3' }, @response
604
+ @session.event_content '1:2:3'
605
+ end
606
+
607
+ it "should return the content body and the X-Dropbox-Metadata header JSON" do
608
+ resp = { 'some' => { 'json' => 123 } }
609
+ body = "Content here"
610
+ @response.stub!(:body).and_return(body)
611
+ @response.should_receive(:header).once.and_return('X-Dropbox-Metadata' => resp.to_json)
612
+ @token_mock.stub!(:get).and_return(@response)
613
+
614
+ @session.event_content('1:2:3').should == [ body, resp ]
615
+ end
616
+ end
617
+
618
+ {
619
+ :account => [ :get ],
620
+ :upload => [ :post, __FILE__, 'path/here' ],
621
+ :copy => [ :post, 'source/file', 'dest/file' ],
622
+ :move => [ :post, 'source/file', 'dest/file' ],
623
+ :create_folder => [ :post, 'new/folder' ],
624
+ :metadata => [ :get, 'some/file' ],
625
+ :event_metadata => [ :get, 'some_json' ]
626
+ }.each do |meth, args|
627
+ describe meth do
628
+ before :each do
629
+ stub_for_upload_testing
630
+ @token_mock.stub!(args.first).and_return(@response)
631
+ end
632
+
633
+ it "should parse the JSON response if successful" do
634
+ @response.stub!(:body).and_return('{"test":"json"}')
635
+ @session.send(meth, *(args[1..-1]))
636
+ end
637
+
638
+ it "should raise a ParseError if the JSON is invalid" do
639
+ @response.stub!(:body).and_return('sdgsdg')
640
+ lambda { @session.send(meth, *(args[1..-1])) }.should raise_error(Dropbox::ParseError)
641
+ end
642
+
643
+ it "should raise UnsuccessfulResponseError if unsuccessful" do
644
+ @response.stub!(:kind_of?).and_return(false)
645
+ lambda { @session.send(meth, *(args[1..-1])) }.should raise_error(Dropbox::UnsuccessfulResponseError)
646
+ end
647
+ end
648
+ end
649
+
650
+ describe "#mode" do
651
+ it "should return the correct mode" do
652
+ @session.mode = :dropbox
653
+ @session.mode.should eql(:dropbox)
654
+
655
+ @session.mode = :sandbox
656
+ @session.mode.should eql(:sandbox)
657
+
658
+ @session.mode = :metadata_only
659
+ @session.mode.should eql(:metadata_only)
660
+ end
661
+
662
+ it "should be :sandbox by default" do
663
+ @session.mode.should eql(:sandbox)
664
+ end
665
+ end
666
+
667
+ describe "#mode=" do
668
+ it "set the API root" do
669
+ @session.mode = :dropbox
670
+ @token_mock.should_receive(:get).once do |url, *rest|
671
+ url.should include('/dropbox')
672
+ url.should_not include('/sandbox')
673
+ @response
674
+ end
675
+ @session.download 'file'
676
+
677
+ @session.mode = :sandbox
678
+ @token_mock.should_receive(:get).once do |url, *rest|
679
+ url.should include('/sandbox')
680
+ url.should_not include('/dropbox')
681
+ @response
682
+ end
683
+ @session.download 'file'
684
+
685
+ @session.mode = :metadata_only
686
+ @token_mock.should_receive(:get).once do |url, *rest|
687
+ url.should include('/dropbox')
688
+ url.should_not include('/sandbox')
689
+ @response
690
+ end
691
+ @session.download 'file'
692
+ end
693
+
694
+ it "should raise for invalid modes" do
695
+ lambda { @session.mode = :foo }.should raise_error(ArgumentError)
696
+ end
697
+ end
698
+
699
+ {
700
+ :account => [],
701
+ :copy => [ 'foo', 'bar' ],
702
+ :create_folder => [ 'foo' ],
703
+ :delete => [ 'foo' ],
704
+ :move => [ 'foo', 'bar' ],
705
+ :link => [ 'foo' ],
706
+ :metadata => [ 'foo'],
707
+ :download => [ 'foo' ],
708
+ :event_metadata => [ 'foo' ],
709
+ :event_content => [ 'foo' ]
710
+ }.each do |root_method, args|
711
+ describe ".#{root_method}" do
712
+ before :each do
713
+ @session = Dropbox::Session.new('foo', 'bar', :ssl => true)
714
+ @session.authorize
715
+
716
+ @token_mock.stub!(:get).and_return(@response)
717
+ @token_mock.stub!(:post).and_return(@response)
718
+ @response.stub!(:body).and_return('{"a":"b"}')
719
+ @response.stub!(:header).and_return('X-Dropbox-Metadata' => '{"a":"b"}')
720
+ end
721
+
722
+ it "should use the SSL host if :ssl => true is given to the constructor" do
723
+ Dropbox.should_receive(:api_url).once do |*args|
724
+ args.last[:ssl].should be_true
725
+ "http://www.example.com/test"
726
+ end
727
+
728
+ @session.send(root_method, *args)
729
+ end
730
+ end
731
+ end
732
+
733
+ {
734
+ :download => [ :get, 'path/to/file' ],
735
+ :copy => [ :post, 'source/file', 'dest/file' ],
736
+ :move => [ :post, 'source/file', 'dest/file' ],
737
+ :create_folder => [ :post, 'new/folder' ],
738
+ :delete => [ :post, 'some/file' ],
739
+ :link => [ :get, 'some/file' ],
740
+ :metadata => [ :get, 'some/file' ],
741
+ :event_metadata => [ :get, 'some_json' ],
742
+ :event_content => [ :get, '1:2:3' ]
743
+ }.each do |root_method, args|
744
+ describe ".#{root_method}" do
745
+ before :each do
746
+ @response.stub!(:body).and_return('{"a":"b"}')
747
+ @response.stub!(:header).and_return('X-Dropbox-Metadata' => '{"a":"b"}')
748
+ end
749
+
750
+ it "should use the dropbox root if in dropbox mode" do
751
+ @token_mock.should_receive(args.first).once do |url, *rest|
752
+ url.should_not include('sandbox')
753
+ @response
754
+ end
755
+ @session.mode = :dropbox
756
+ @session.send(root_method, *(args[1..-1]))
757
+ end
758
+
759
+ it "should use the sandbox root if in sandbox mode" do
760
+ @token_mock.should_receive(args.first).once do |url, *rest|
761
+ url.should include('sandbox')
762
+ @response
763
+ end
764
+ @session.mode = :sandbox
765
+ @session.send(root_method, *(args[1..-1]))
766
+ end
767
+
768
+ it "should use the dropbox root if in metadata mode" do
769
+ @token_mock.should_receive(args.first).once do |url, *rest|
770
+ url.should_not include('sandbox')
771
+ @response
772
+ end
773
+ @session.mode = :metadata_only
774
+ @session.send(root_method, *(args[1..-1]))
775
+ end
776
+ end
777
+ end
778
+ end