songkick-transport 0.2.2 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,18 +1,18 @@
1
1
  module Songkick
2
2
  module Transport
3
3
  module Serialization
4
-
4
+
5
5
  extend self
6
-
6
+
7
7
  SANITIZED_VALUE = '[REMOVED]'
8
-
8
+
9
9
  def build_url(verb, host, path, params, scrub=false)
10
10
  url = host + path
11
11
  return url if USE_BODY.include?(verb)
12
12
  qs = build_query_string(params, true, scrub)
13
13
  url + (qs == '' ? '' : '?' + qs)
14
14
  end
15
-
15
+
16
16
  def build_query_string(params, fully_encode = true, sanitize = false)
17
17
  return params if String === params
18
18
  pairs = []
@@ -21,7 +21,7 @@ module Songkick
21
21
  pairs << [key, value]
22
22
  end
23
23
  if fully_encode
24
- pairs.map { |p| p.join('=') }.join('&')
24
+ pairs.map { |p| p.join('=') }.join('&')
25
25
  else
26
26
  pairs.inject({}) do |hash, pair|
27
27
  hash[pair.first] = pair.last
@@ -29,7 +29,7 @@ module Songkick
29
29
  end
30
30
  end
31
31
  end
32
-
32
+
33
33
  def each_qs_param(prefix, value, fully_encode = true, &block)
34
34
  case value
35
35
  when Array
@@ -47,7 +47,7 @@ module Songkick
47
47
  block.call(prefix, encoded)
48
48
  end
49
49
  end
50
-
50
+
51
51
  def multipart?(params)
52
52
  case params
53
53
  when Hash then params.any? { |k,v| multipart? v }
@@ -55,25 +55,25 @@ module Songkick
55
55
  else Transport::IO === params
56
56
  end
57
57
  end
58
-
58
+
59
59
  def sanitize?(key)
60
60
  Transport.sanitized_params.any? { |param| param === key }
61
61
  end
62
-
62
+
63
63
  def serialize_multipart(params, boundary = Multipartable::DEFAULT_BOUNDARY)
64
64
  params = build_query_string(params, false)
65
-
65
+
66
66
  parts = params.map { |k,v| Parts::Part.new(boundary, k, v) }
67
67
  parts << Parts::EpiloguePart.new(boundary)
68
68
  ios = parts.map { |p| p.to_io }
69
-
69
+
70
70
  {
71
71
  :content_type => "multipart/form-data; boundary=#{boundary}",
72
72
  :content_length => parts.inject(0) { |sum,i| sum + i.length }.to_s,
73
73
  :body => CompositeReadIO.new(*ios).read
74
74
  }
75
75
  end
76
-
76
+
77
77
  end
78
78
  end
79
79
  end
@@ -0,0 +1,89 @@
1
+ module Songkick
2
+ module Transport
3
+ class Service
4
+ def self.endpoint(name)
5
+ @endpoint_name = name.to_s
6
+ end
7
+
8
+ def self.timeout(value)
9
+ @timeout = value
10
+ end
11
+
12
+ def self.user_agent(value)
13
+ @user_agent = value
14
+ end
15
+
16
+ def self.transport_layer(value)
17
+ @transport_layer = value
18
+ end
19
+
20
+ def self.stub_transport(stub)
21
+ @stub_transport = stub
22
+ end
23
+
24
+ def self.set_endpoints(hash)
25
+ unless self == Songkick::Transport::Service
26
+ raise "set_endpoints only on Songkick::Transport::Service"
27
+ end
28
+ @endpoints = hash
29
+ end
30
+
31
+ def self.ancestor
32
+ self.ancestors.select {|a| a.respond_to?(:get_user_agent)}[1]
33
+ end
34
+
35
+ def self.get_endpoint_name
36
+ @endpoint_name || (ancestor && ancestor.get_endpoint_name)
37
+ end
38
+
39
+ def self.get_timeout
40
+ @timeout || (ancestor && ancestor.get_timeout) || 10
41
+ end
42
+
43
+ def self.get_user_agent
44
+ @user_agent || (ancestor && ancestor.get_user_agent)
45
+ end
46
+
47
+ def self.get_endpoints
48
+ @endpoints || {}
49
+ end
50
+
51
+ def self.get_transport_layer
52
+ @transport_layer || (ancestor && ancestor.get_transport_layer) || Songkick::Transport::Curb
53
+ end
54
+
55
+ def self.get_stub_transport
56
+ @stub_transport || (ancestor && ancestor.get_stub_transport) || nil
57
+ end
58
+
59
+ def self.new_transport
60
+ unless name = get_endpoint_name
61
+ raise "no endpoint specified for #{self}, call endpoint 'foo' inside #{self}"
62
+ end
63
+ unless endpoint = Service.get_endpoints[name]
64
+ raise "can't find endpoint for '#{name}', should have called Songkick::Transport::Service.set_endpoints"
65
+ end
66
+ unless user_agent = get_user_agent
67
+ raise "no user agent specified for #{self}, call user_agent 'foo' inside #{self} or on Songkick::Transport::Service"
68
+ end
69
+ get_stub_transport || get_transport_layer.new(endpoint, :user_agent => user_agent,
70
+ :timeout => get_timeout)
71
+ end
72
+
73
+ def http
74
+ @http ||= self.class.new_transport
75
+ end
76
+
77
+ def stub_transport(http)
78
+ @http = http
79
+ end
80
+
81
+ def rescue_404(response=nil)
82
+ yield
83
+ rescue Songkick::Transport::HttpError => e
84
+ e.status == 404 ? response : (raise e)
85
+ end
86
+
87
+ end
88
+ end
89
+ end
@@ -72,5 +72,17 @@ describe Songkick::Transport::Response do
72
72
  response.errors.should == []
73
73
  end
74
74
  end
75
+
76
+ describe "422 with customer user error codes" do
77
+ let(:response) { process("", 422, {"Content-Type" => "application/json"}, '{"errors":[]}', [409, 422]) }
78
+
79
+ it "is a UserError" do
80
+ response.should be_a(Songkick::Transport::Response::UserError)
81
+ end
82
+
83
+ it "exposes the errors" do
84
+ response.errors.should == []
85
+ end
86
+ end
75
87
  end
76
88
 
@@ -1,206 +1,219 @@
1
1
  require "spec_helper"
2
2
 
3
- describe Songkick::Transport do
4
- shared_examples_for "transport" do
5
- before { TestApp.listen(4567) }
6
- after { TestApp.stop }
7
-
8
- describe :get do
9
- it "retrieves data using GET" do
10
- transport.get("/artists/99").data.should == {"id" => 99}
11
- end
12
-
13
- it "exposes the response headers" do
14
- transport.get("/artists/99").headers["content-type"].should == "application/json"
15
- end
16
-
17
- it "can send array params" do
18
- transport.get("/", :list => %w[a b]).data.should == {"list" => ["a", "b"]}
19
- end
20
-
21
- it "can send hash params" do
22
- transport.get("/", :list => {:a => "b"}).data.should == {"list" => {"a" => "b"}}
23
- end
24
-
25
- it "can send nested data structures" do
26
- structure = {
27
- "hash" => {"a" => {"b" => ["c", "d"], "e" => "f"}},
28
- "lisp" => ["define", {"square" => ["x", "y"]}, "*", "x", "x"]
29
- }
30
- transport.get("/", structure).data.should == structure
31
- end
32
-
33
- it "raises an UpstreamError for a nonexistent resource" do
34
- lambda { transport.get("/nothing") }.should raise_error(Songkick::Transport::UpstreamError)
35
- end
36
-
37
- it "raises an UpstreamError for a POST resource" do
38
- lambda { transport.get("/artists") }.should raise_error(Songkick::Transport::UpstreamError)
39
- end
40
-
41
- it "raises an UpstreamError for invalid JSON" do
42
- lambda { transport.get("/invalid") }.should raise_error(Songkick::Transport::InvalidJSONError)
43
- end
44
- end
45
-
46
- describe :with_headers do
47
- it "adds the given headers to requests" do
48
- data = transport.with_headers("Authorization" => "correct password").get("/authenticate").data
49
- data.should == {"successful" => true}
50
- end
51
-
52
- it "reformats Rack-style headers" do
53
- data = transport.with_headers("HTTP_AUTHORIZATION" => "correct password").get("/authenticate").data
54
- data.should == {"successful" => true}
55
- end
56
-
57
- it "does not affect requests made directly on the transport object" do
58
- transport.with_headers("Authorization" => "correct password").get("/authenticate")
59
- data = transport.get("/authenticate").data
60
- data.should == {"successful" => false}
61
- end
62
-
63
- it "can set Content-Type" do
64
- data = transport.with_headers("Content-Type" => "application/json").post("/content").data
65
- data.should == {"type" => "application/json"}
66
- end
67
- end
68
-
69
- describe :options do
70
- it "sends an OPTIONS request" do
71
- response = transport.options("/.well-known/host-meta")
72
- response.headers["Access-Control-Allow-Methods"].should == "GET, PUT, DELETE"
73
- end
74
- end
75
-
76
- describe :post do
77
- it "sends data using POST" do
78
- data = transport.post("/artists", :name => "Amon Tobin").data
79
- data.should == {"id" => "new", "name" => "AMON TOBIN"}
80
- end
81
-
82
- it "can send array params" do
83
- transport.post("/", :list => %w[a b]).data.should == {"list" => ["a", "b"]}
84
- end
85
-
86
- it "can send hash params" do
87
- transport.post("/", :list => {:a => "b"}).data.should == {"list" => {"a" => "b"}}
88
- end
89
-
90
- it "can send a raw body" do
91
- response = transport.with_headers("Content-Type" => "text/plain").post("/process", "Hello, world!")
92
- response.data.should == {"body" => "Hello, world!", "type" => "text/plain"}
93
- end
94
-
95
- it "raises an UpstreamError for a PUT resource" do
96
- lambda { transport.post("/artists/64") }.should raise_error(Songkick::Transport::UpstreamError)
97
- end
98
- end
99
-
100
- describe :put do
101
- it "sends data using PUT" do
102
- data = transport.put("/artists/64", :name => "Amon Tobin").data
103
- data.should == {"id" => 64, "name" => "amon tobin"}
104
- end
105
-
106
- it "raises an UpstreamError for a POST resource" do
107
- lambda{transport.put("/artists")}.should raise_error(Songkick::Transport::UpstreamError)
108
- end
109
- end
110
-
111
- describe "file uploads" do
112
- before do
113
- pending if Songkick::Transport::RackTest === transport
114
- end
115
-
116
- after { file.close }
117
-
118
- let(:file) { File.open(File.expand_path("../../songkick.png", __FILE__)) }
119
- let(:upload) { Songkick::Transport::IO.new(file, "image/jpeg", "songkick.png") }
120
-
121
- let :params do
122
- {:concert => {:file => upload, :foo => "me@thing.com"}}
123
- end
124
-
125
- let :expected_response do
126
- {
127
- "filename" => "songkick.png",
128
- "method" => @http_method,
129
- "size" => 6694,
130
- "foo" => "me@thing.com"
131
- }
132
- end
133
-
134
- it "uploads files using POST" do
135
- @http_method = "post"
136
- response = transport.post('/upload', params)
137
- response.status.should == 200
138
- response.data.should == expected_response
139
- end
140
-
141
- it "uploads files using PUT" do
142
- @http_method = "put"
143
- response = transport.put('/upload', params)
144
- response.status.should == 200
145
- response.data.should == expected_response
146
- end
147
- end
148
-
149
- describe "reporting" do
150
- before do
151
- @report = Songkick::Transport.report
152
- end
153
-
154
- it "executes a block and returns its value" do
155
- @report.execute { transport.get("/artists/99").data }.should == {"id" => 99}
156
- end
157
-
158
- it "reports a successful request" do
159
- @report.execute { transport.get("/artists/99") }
160
- @report.size.should == 1
161
-
162
- request = @report.first
163
- request.endpoint.should == endpoint
164
- request.http_method.should == "get"
165
- request.path.should == "/artists/99"
166
- request.response.data.should == {"id" => 99}
167
- end
168
-
169
- it "reports a failed request" do
170
- @report.execute { transport.get("/invalid") } rescue nil
171
- @report.size.should == 1
172
-
173
- request = @report.first
174
- request.http_method.should == "get"
175
- request.path.should == "/invalid"
176
- request.response.should == nil
177
- request.error.should be_a(Songkick::Transport::InvalidJSONError)
178
- end
179
- it "reports the total duration" do
180
- @report.execute { transport.get("/artists/99") }
181
- request = @report.first
182
- request.stub(:duration).and_return 3.14
183
- @report.total_duration.should == 3.14
184
- end
3
+ shared_examples_for "Songkick::Transport" do
4
+ before(:all) { TestApp.listen(4567) }
5
+ after(:all) { TestApp.stop }
6
+
7
+ describe :get do
8
+ it "retrieves data using GET" do
9
+ transport.get("/artists/99").data.should == {"id" => 99}
10
+ end
11
+
12
+ it "exposes the response headers" do
13
+ transport.get("/artists/99").headers["content-type"].should == "application/json"
14
+ end
15
+
16
+ it "can send array params" do
17
+ transport.get("/", :list => %w[a b]).data.should == {"list" => ["a", "b"]}
18
+ end
19
+
20
+ it "can send hash params" do
21
+ transport.get("/", :list => {:a => "b"}).data.should == {"list" => {"a" => "b"}}
22
+ end
23
+
24
+ it "can send nested data structures" do
25
+ structure = {
26
+ "hash" => {"a" => {"b" => ["c", "d"], "e" => "f"}},
27
+ "lisp" => ["define", {"square" => ["x", "y"]}, "*", "x", "x"]
28
+ }
29
+ transport.get("/", structure).data.should == structure
30
+ end
31
+
32
+ it "raises an UpstreamError for a nonexistent resource" do
33
+ lambda { transport.get("/nothing") }.should raise_error(Songkick::Transport::UpstreamError)
34
+ end
35
+
36
+ it "raises an UpstreamError for a POST resource" do
37
+ lambda { transport.get("/artists") }.should raise_error(Songkick::Transport::UpstreamError)
38
+ end
39
+
40
+ it "raises an UpstreamError for invalid JSON" do
41
+ lambda { transport.get("/invalid") }.should raise_error(Songkick::Transport::InvalidJSONError)
42
+ end
43
+ end
44
+
45
+ describe :with_headers do
46
+ it "adds the given headers to requests" do
47
+ data = transport.with_headers("Authorization" => "correct password").get("/authenticate").data
48
+ data.should == {"successful" => true}
49
+ end
50
+
51
+ it "reformats Rack-style headers" do
52
+ data = transport.with_headers("HTTP_AUTHORIZATION" => "correct password").get("/authenticate").data
53
+ data.should == {"successful" => true}
54
+ end
55
+
56
+ it "does not affect requests made directly on the transport object" do
57
+ transport.with_headers("Authorization" => "correct password").get("/authenticate")
58
+ data = transport.get("/authenticate").data
59
+ data.should == {"successful" => false}
60
+ end
61
+
62
+ it "can set Content-Type" do
63
+ data = transport.with_headers("Content-Type" => "application/json").post("/content").data
64
+ data.should == {"type" => "application/json"}
65
+ end
66
+ end
67
+
68
+ describe :options do
69
+ it "sends an OPTIONS request" do
70
+ response = transport.options("/.well-known/host-meta")
71
+ response.headers["Access-Control-Allow-Methods"].should == "GET, PUT, DELETE"
72
+ end
73
+ end
74
+
75
+ describe :post do
76
+ it "sends data using POST" do
77
+ data = transport.post("/artists", :name => "Amon Tobin").data
78
+ data.should == {"id" => "new", "name" => "AMON TOBIN"}
79
+ end
80
+
81
+ it "can send array params" do
82
+ transport.post("/", :list => %w[a b]).data.should == {"list" => ["a", "b"]}
83
+ end
84
+
85
+ it "can send hash params" do
86
+ transport.post("/", :list => {:a => "b"}).data.should == {"list" => {"a" => "b"}}
87
+ end
88
+
89
+ it "can send a raw body" do
90
+ response = transport.with_headers("Content-Type" => "text/plain").post("/process", "Hello, world!")
91
+ response.data.should == {"body" => "Hello, world!", "type" => "text/plain"}
92
+ end
93
+
94
+ it "raises an UpstreamError for a PUT resource" do
95
+ lambda { transport.post("/artists/64") }.should raise_error(Songkick::Transport::UpstreamError)
96
+ end
97
+ end
98
+
99
+ describe :put do
100
+ it "sends data using PUT" do
101
+ data = transport.put("/artists/64", :name => "Amon Tobin").data
102
+ data.should == {"id" => 64, "name" => "amon tobin"}
103
+ end
104
+
105
+ it "raises an UpstreamError for a POST resource" do
106
+ lambda{transport.put("/artists")}.should raise_error(Songkick::Transport::UpstreamError)
185
107
  end
186
108
  end
187
-
188
- describe Songkick::Transport::Curb do
189
- let(:endpoint) { "http://localhost:4567" }
190
- let(:transport) { Songkick::Transport::Curb.new(endpoint) }
191
- it_should_behave_like "transport"
109
+
110
+ describe "file uploads" do
111
+ before do
112
+ pending if Songkick::Transport::RackTest === transport
113
+ end
114
+
115
+ after { file.close }
116
+
117
+ let(:file) { File.open(File.expand_path("../../songkick.png", __FILE__)) }
118
+ let(:upload) { Songkick::Transport::IO.new(file, "image/jpeg", "songkick.png") }
119
+
120
+ let :params do
121
+ {:concert => {:file => upload, :foo => "me@thing.com"}}
122
+ end
123
+
124
+ let :expected_response do
125
+ {
126
+ "filename" => "songkick.png",
127
+ "method" => @http_method,
128
+ "size" => 6694,
129
+ "foo" => "me@thing.com"
130
+ }
131
+ end
132
+
133
+ it "uploads files using POST" do
134
+ @http_method = "post"
135
+ response = transport.post('/upload', params)
136
+ response.status.should == 200
137
+ response.data.should == expected_response
138
+ end
139
+
140
+ it "uploads files using PUT" do
141
+ @http_method = "put"
142
+ response = transport.put('/upload', params)
143
+ response.status.should == 200
144
+ response.data.should == expected_response
145
+ end
192
146
  end
193
-
194
- describe Songkick::Transport::HttParty do
195
- let(:endpoint) { "http://localhost:4567" }
196
- let(:transport) { Songkick::Transport::HttParty.new(endpoint) }
197
- it_should_behave_like "transport"
147
+
148
+ describe "reporting" do
149
+ before do
150
+ Songkick::Transport::Reporting.start
151
+ @report = Songkick::Transport.report
152
+ end
153
+
154
+ it "executes a block and returns its value" do
155
+ @report.execute { transport.get("/artists/99").data }.should == {"id" => 99}
156
+ end
157
+
158
+ it "reports a successful request" do
159
+ @report.execute { transport.get("/artists/99") }
160
+ @report.size.should == 1
161
+
162
+ request = @report.first
163
+ request.endpoint.should == endpoint
164
+ request.http_method.should == "get"
165
+ request.path.should == "/artists/99"
166
+ request.response.data.should == {"id" => 99}
167
+ end
168
+
169
+ it "reports a failed request" do
170
+ @report.execute { transport.get("/invalid") } rescue nil
171
+ @report.size.should == 1
172
+
173
+ request = @report.first
174
+ request.http_method.should == "get"
175
+ request.path.should == "/invalid"
176
+ request.response.should == nil
177
+ request.error.should be_a(Songkick::Transport::InvalidJSONError)
178
+ end
179
+
180
+ it "reports the total duration" do
181
+ @report.execute { transport.get("/artists/99") }
182
+ request = @report.first
183
+ request.stub(:duration).and_return 3.14
184
+ @report.total_duration.should == 3.14
185
+ end
198
186
  end
199
-
200
- describe Songkick::Transport::RackTest do
201
- let(:endpoint) { TestApp }
202
- let(:transport) { Songkick::Transport::RackTest.new(endpoint) }
203
- it_should_behave_like "transport"
187
+
188
+ describe "error_status_codes" do
189
+ let(:codes) { [409, 422] }
190
+
191
+ it "can be provided on initialization" do
192
+ transport = described_class.new(endpoint, :user_error_codes => codes)
193
+ transport.user_error_codes.should == codes
194
+ end
195
+
196
+ it "default to 409" do
197
+ transport.user_error_codes.should == [409]
198
+ end
204
199
  end
205
200
  end
206
201
 
202
+ describe Songkick::Transport::Curb do
203
+ let(:endpoint) { "http://localhost:4567" }
204
+ let(:transport) { Songkick::Transport::Curb.new(endpoint) }
205
+ it_should_behave_like "Songkick::Transport"
206
+ end
207
+
208
+ describe Songkick::Transport::HttParty do
209
+ let(:endpoint) { "http://localhost:4567" }
210
+ let(:transport) { Songkick::Transport::HttParty.new(endpoint) }
211
+ it_should_behave_like "Songkick::Transport"
212
+ end
213
+
214
+ describe Songkick::Transport::RackTest do
215
+ let(:endpoint) { TestApp }
216
+ let(:transport) { Songkick::Transport::RackTest.new(endpoint) }
217
+ it_should_behave_like "Songkick::Transport"
218
+ end
219
+