condenser 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +1 -0
  4. data/lib/condenser.rb +108 -0
  5. data/lib/condenser/asset.rb +221 -0
  6. data/lib/condenser/cache/memory_store.rb +92 -0
  7. data/lib/condenser/cache/null_store.rb +37 -0
  8. data/lib/condenser/context.rb +272 -0
  9. data/lib/condenser/encoding_utils.rb +155 -0
  10. data/lib/condenser/environment.rb +50 -0
  11. data/lib/condenser/errors.rb +11 -0
  12. data/lib/condenser/export.rb +68 -0
  13. data/lib/condenser/manifest.rb +89 -0
  14. data/lib/condenser/pipeline.rb +82 -0
  15. data/lib/condenser/processors/babel.min.js +25 -0
  16. data/lib/condenser/processors/babel_processor.rb +87 -0
  17. data/lib/condenser/processors/node_processor.rb +38 -0
  18. data/lib/condenser/processors/rollup.js +24083 -0
  19. data/lib/condenser/processors/rollup_processor.rb +164 -0
  20. data/lib/condenser/processors/sass_importer.rb +81 -0
  21. data/lib/condenser/processors/sass_processor.rb +300 -0
  22. data/lib/condenser/resolve.rb +202 -0
  23. data/lib/condenser/server.rb +307 -0
  24. data/lib/condenser/templating_engine/erb.rb +21 -0
  25. data/lib/condenser/utils.rb +32 -0
  26. data/lib/condenser/version.rb +3 -0
  27. data/lib/condenser/writers/file_writer.rb +28 -0
  28. data/lib/condenser/writers/zlib_writer.rb +42 -0
  29. data/test/cache_test.rb +24 -0
  30. data/test/environment_test.rb +49 -0
  31. data/test/manifest_test.rb +513 -0
  32. data/test/pipeline_test.rb +31 -0
  33. data/test/preprocessor/babel_test.rb +21 -0
  34. data/test/processors/rollup_test.rb +71 -0
  35. data/test/resolve_test.rb +105 -0
  36. data/test/server_test.rb +361 -0
  37. data/test/templates/erb_test.rb +18 -0
  38. data/test/test_helper.rb +68 -0
  39. data/test/transformers/scss_test.rb +49 -0
  40. metadata +193 -0
@@ -0,0 +1,31 @@
1
+ require 'test_helper'
2
+
3
+ class PipelineTest < ActiveSupport::TestCase
4
+
5
+ def setup
6
+ super
7
+ @env.clear_pipeline
8
+ end
9
+
10
+ test 'clear_pipeline' do
11
+ @env.register_mime_type 'application/javascript', extension: 'js', charset: :unicode
12
+ @env.register_template 'application/erb', PipelineTest
13
+ @env.register_preprocessor 'application/javascript', PipelineTest
14
+ @env.register_transformer 'application/scss', 'application/css', PipelineTest
15
+ @env.register_postprocessor 'application/javascript', PipelineTest
16
+ @env.register_minifier 'application/javascript', PipelineTest
17
+ @env.register_writer '*/*', PipelineTest
18
+
19
+ vars = %w(templates preprocessors transformers postprocessors minifiers writers)
20
+ vars.each do |var|
21
+ assert_not_empty @env.send(var)
22
+ end
23
+
24
+ @env.clear_pipeline
25
+
26
+ vars.each do |var|
27
+ assert_empty @env.send(var)
28
+ end
29
+ end
30
+
31
+ end
@@ -0,0 +1,21 @@
1
+ require 'test_helper'
2
+
3
+ class CondenserBabelTest < ActiveSupport::TestCase
4
+
5
+ test 'find' do
6
+ file 'name.js', <<~JS
7
+ var t = { 'var': () => { return 2; } };
8
+
9
+ export {t as name1};
10
+ JS
11
+
12
+ assert_file 'name.js', 'application/javascript', <<~JS
13
+ var t = { 'var': function _var() {
14
+ return 2;
15
+ } };
16
+
17
+ export { t as name1 };
18
+ JS
19
+ end
20
+
21
+ end
@@ -0,0 +1,71 @@
1
+ require 'test_helper'
2
+
3
+ class RollupTest < ActiveSupport::TestCase
4
+
5
+ def setup
6
+ super
7
+ @env.unregister_preprocessor('application/javascript', Condenser::BabelProcessor)
8
+ end
9
+
10
+ test 'import file' do
11
+ file 'main.js', <<~JS
12
+ import { cube } from './math.js';
13
+
14
+ console.log( cube( 5 ) ); // 125
15
+ JS
16
+ file 'math.js', <<~JS
17
+
18
+ // This function isn't used anywhere, so
19
+ // Rollup excludes it from the bundle...
20
+ export function square ( x ) {
21
+ return x * x;
22
+ }
23
+
24
+ // This function gets included
25
+ export function cube ( x ) {
26
+ return x * x * x;
27
+ }
28
+ JS
29
+
30
+ assert_exported_file 'main.js', 'application/javascript', <<~FILE
31
+ (function () {
32
+ 'use strict';
33
+
34
+ // This function gets included
35
+ function cube ( x ) {
36
+ return x * x * x;
37
+ }
38
+
39
+ console.log( cube( 5 ) ); // 125
40
+
41
+ }());
42
+ FILE
43
+ end
44
+
45
+ test 'import an erb file' do
46
+ file 'main.js', <<~JS
47
+ import { cube } from './math.js';
48
+
49
+ console.log( cube( 5 ) ); // 125
50
+ JS
51
+ file 'math.js.erb', <<~JS
52
+ export function cube ( x ) {
53
+ return <%= 2 %> * x * x;
54
+ }
55
+ JS
56
+
57
+ assert_exported_file 'main.js', 'application/javascript', <<~FILE
58
+ (function () {
59
+ 'use strict';
60
+
61
+ function cube ( x ) {
62
+ return 2 * x * x;
63
+ }
64
+
65
+ console.log( cube( 5 ) ); // 125
66
+
67
+ }());
68
+ FILE
69
+ end
70
+
71
+ end
@@ -0,0 +1,105 @@
1
+ require 'test_helper'
2
+
3
+ class ResolveTest < ActiveSupport::TestCase
4
+
5
+ def setup
6
+ super
7
+ @env.clear_pipeline
8
+ @env.register_template 'application/erb', Condenser::Erubi
9
+ @env.register_transformer 'text/scss', 'text/css', Condenser::Erubi
10
+ @env.register_exporter 'application/javascript', Condenser::RollupProcessor
11
+ @env.register_writer 'application/javascript', Condenser::Erubi, 'application/gzip'
12
+ end
13
+
14
+ test 'decompose_path' do
15
+ assert_equal [nil, 'test', ['.text'], ['text/plain']], @env.decompose_path('test.text')
16
+
17
+ assert_equal ['dir', 'test', ['.text'], ['text/plain']], @env.decompose_path('dir/test.text')
18
+
19
+ assert_equal ['dir', 'test.unkownmime', ['.text'], ['text/plain']], @env.decompose_path('dir/test.unkownmime.text')
20
+
21
+ assert_equal [nil, '*', nil, []], @env.decompose_path('*')
22
+ assert_equal ['*', '*', nil, []], @env.decompose_path('*/*')
23
+ assert_equal ['**', '*', nil, []], @env.decompose_path('**/*')
24
+
25
+ assert_equal ['test', '*', nil, []], @env.decompose_path('test/*')
26
+ assert_equal ['test/*', '*', nil, []], @env.decompose_path('test/*/*')
27
+ assert_equal ['test/**', '*', nil, []], @env.decompose_path('test/**/*')
28
+
29
+ assert_equal [nil, '*', ['.js'], ['application/javascript']], @env.decompose_path('*.js')
30
+ assert_equal ['*', '*', ['.js'], ['application/javascript']], @env.decompose_path('*/*.js')
31
+ assert_equal ['**', '*', ['.js'], ['application/javascript']], @env.decompose_path('**/*.js')
32
+
33
+ assert_equal ['test', '*', ['.js'], ['application/javascript']], @env.decompose_path('test/*.js')
34
+ assert_equal ['test/*', '*', ['.js'], ['application/javascript']], @env.decompose_path('test/*/*.js')
35
+ assert_equal ['test/**', '*', ['.js'], ['application/javascript']], @env.decompose_path('test/**/*.js')
36
+ end
37
+
38
+ test 'resolve' do
39
+ file 'file.js', 'test'
40
+ file 'test/file.scss', 'test'
41
+ file 'test/z/file.js', 'test'
42
+
43
+ assert_file('file.js', 'application/javascript')
44
+ assert_file('test/file.css', 'text/css')
45
+
46
+ assert_equal %w(file.js test/file.css test/z/file.js), @env.resolve('**/*').map(&:filename)
47
+ assert_equal %w(file.js test/file.css test/z/file.js), @env.resolve('**/*', accept: ['text/css', 'application/javascript']).map(&:filename)
48
+ assert_equal %w(file.js), @env.resolve('*', accept: ['application/javascript']).map(&:filename)
49
+ assert_equal %w(file.js test/z/file.js), @env.resolve('**/*', accept: ['application/javascript']).map(&:filename)
50
+ assert_equal %w(file.js), @env.resolve('*.js').map(&:filename)
51
+ assert_equal %w(file.js test/z/file.js), @env.resolve('**/*.js').map(&:filename)
52
+
53
+
54
+ assert_equal %w(test/file.css), @env.resolve('test/*').map(&:filename)
55
+ assert_equal %w(test/file.css), @env.resolve('test/*', accept: ['text/css', 'application/javascript']).map(&:filename)
56
+ assert_equal %w(test/file.css test/z/file.js), @env.resolve('test/**/*').map(&:filename)
57
+ assert_equal %w(test/file.css test/z/file.js), @env.resolve('test/**/*', accept: ['text/css', 'application/javascript']).map(&:filename)
58
+ assert_equal %w(test/z/file.js), @env.resolve('test/z/*', accept: ['application/javascript']).map(&:filename)
59
+ assert_equal %w(test/z/file.js), @env.resolve('test/**/*', accept: ['application/javascript']).map(&:filename)
60
+ assert_equal %w(test/z/file.js), @env.resolve('test/z/*.js').map(&:filename)
61
+ assert_equal %w(test/z/file.js), @env.resolve('test/**/*.js').map(&:filename)
62
+ end
63
+
64
+ test 'relative require' do
65
+ file 'a/main.js', <<~JS
66
+ import { cube } from './math.js';
67
+ console.log( cube( 5 ) ); // 125
68
+ JS
69
+ file 'a/math.js', <<~JS
70
+ export function cube ( x ) {
71
+ return x * x * x;
72
+ }
73
+ JS
74
+
75
+ file 'b/math.js', <<~JS
76
+ export function square ( x ) {
77
+ return x * x;
78
+ }
79
+ JS
80
+
81
+ @env.append_path File.join(@path, 'b')
82
+ @env.append_path File.join(@path, 'a')
83
+
84
+
85
+ assert_exported_file('main.js', 'application/javascript', <<~JS)
86
+ (function () {
87
+ 'use strict';
88
+
89
+ function cube ( x ) {
90
+ return x * x * x;
91
+ }
92
+
93
+ console.log( cube( 5 ) ); // 125
94
+
95
+ }());
96
+ JS
97
+ end
98
+
99
+ test 'resolve! raises NotFound' do
100
+ assert_raises Condenser::FileNotFound do
101
+ @env.resolve!('**/*')
102
+ end
103
+ end
104
+
105
+ end
@@ -0,0 +1,361 @@
1
+ require 'test_helper'
2
+ require 'rack/builder'
3
+ require 'rack/test'
4
+
5
+ class ServerTest < ActiveSupport::TestCase
6
+ include Rack::Test::Methods
7
+
8
+ def setup
9
+ super
10
+ server = Condenser::Server.new(@env)
11
+ @app = nil
12
+ @condenser_server = Rack::Builder.new do
13
+ map "/assets" do
14
+ run server
15
+ end
16
+ end
17
+
18
+ file 'foo.js', <<~JS
19
+ console.log(1);
20
+ JS
21
+ end
22
+
23
+ def app
24
+ @app ||= Rack::Lint.new(@condenser_server)
25
+ end
26
+
27
+ test "serve single source file" do
28
+ get "/assets/foo.js"
29
+ assert_equal 200, last_response.status
30
+ assert_equal "53", last_response.headers['Content-Length']
31
+ assert_equal "Accept-Encoding", last_response.headers['Vary']
32
+ assert_equal <<~JS, last_response.body
33
+ (function () {
34
+ 'use strict';
35
+
36
+ console.log(1);
37
+
38
+ }());
39
+ JS
40
+ end
41
+
42
+ # TODO:
43
+ # test "serve single source file from cached environment" do
44
+ # get "/cached/javascripts/foo.js"
45
+ # assert_equal "var foo;\n", last_response.body
46
+ # end
47
+
48
+ test "serve source with dependencies" do
49
+ file 'main.js', <<~JS
50
+ import { cube } from './math.js';
51
+
52
+ console.log( cube( 5 ) ); // 125
53
+ JS
54
+ file 'math.js', <<~JS
55
+ export function cube ( x ) {
56
+ return x * x * x;
57
+ }
58
+ JS
59
+
60
+ get "/assets/main.js"
61
+ assert_equal <<~JS, last_response.body
62
+ (function () {
63
+ 'use strict';
64
+
65
+ function cube(x) {
66
+ return x * x * x;
67
+ }
68
+
69
+ console.log(cube(5)); // 125
70
+
71
+ }());
72
+ JS
73
+ end
74
+
75
+ test "serve source with content type headers" do
76
+ file 'main.css', <<~CSS
77
+ body { background: green ; }
78
+ CSS
79
+
80
+ get "/assets/foo.js"
81
+ assert_equal "application/javascript", last_response.headers['Content-Type']
82
+
83
+ get "/assets/main.css"
84
+ assert_equal "text/css; charset=utf-8", last_response.headers['Content-Type']
85
+ end
86
+
87
+ test "serve source with etag headers" do
88
+ get "/assets/foo.js"
89
+
90
+ digest = '85b4185243c9cf4935e686f910a410762c2632a044118ac7ef030094be635c18'
91
+ assert_equal "\"#{digest}\"", last_response.headers['ETag']
92
+ end
93
+
94
+ test "not modified partial response when if-none-match etags match" do
95
+ get "/assets/foo.js"
96
+ assert_equal 200, last_response.status
97
+ etag, cache_control, expires, vary = last_response.headers.values_at(
98
+ 'ETag', 'Cache-Control', 'Expires', 'Vary'
99
+ )
100
+
101
+ assert_nil expires
102
+ get "/assets/foo.js", {},
103
+ 'HTTP_IF_NONE_MATCH' => etag
104
+
105
+ assert_equal 304, last_response.status
106
+
107
+ # Allow 304 headers
108
+ assert_equal cache_control, last_response.headers['Cache-Control']
109
+ assert_equal etag, last_response.headers['ETag']
110
+ assert_nil last_response.headers['Expires']
111
+ assert_equal vary, last_response.headers['Vary']
112
+
113
+ # Disallowed 304 headers
114
+ refute last_response.headers['Content-Type']
115
+ refute last_response.headers['Content-Length']
116
+ refute last_response.headers['Content-Encoding']
117
+ end
118
+
119
+ test "response when if-none-match etags don't match" do
120
+ get "/assets/foo.js", {},
121
+ 'HTTP_IF_NONE_MATCH' => "nope"
122
+
123
+ assert_equal 200, last_response.status
124
+ assert_equal '"85b4185243c9cf4935e686f910a410762c2632a044118ac7ef030094be635c18"', last_response.headers['ETag']
125
+ assert_equal '53', last_response.headers['Content-Length']
126
+ end
127
+
128
+ test "not modified partial response with fingerprint and if-none-match etags match" do
129
+ get "/assets/foo.js"
130
+ assert_equal 200, last_response.status
131
+
132
+ etag = last_response.headers['ETag']
133
+ digest = etag[/"(.+)"/, 1]
134
+
135
+ get "/assets/foo-#{digest}.js", {},
136
+ 'HTTP_IF_NONE_MATCH' => etag
137
+ assert_equal 304, last_response.status
138
+ end
139
+
140
+ test "ok response with fingerprint and if-nonematch etags don't match" do
141
+ get "/assets/foo.js"
142
+ assert_equal 200, last_response.status
143
+
144
+ etag = last_response.headers['ETag']
145
+ digest = etag[/"(.+)"/, 1]
146
+
147
+ get "/assets/foo-#{digest}.js", {},
148
+ 'HTTP_IF_NONE_MATCH' => "nope"
149
+ assert_equal 200, last_response.status
150
+ end
151
+
152
+ test "not found with if-none-match" do
153
+ get "/assets/missing.js", {},
154
+ 'HTTP_IF_NONE_MATCH' => '"000"'
155
+ assert_equal 404, last_response.status
156
+ end
157
+
158
+ test "not found fingerprint with if-none-match" do
159
+ get "/assets/missing-b452c9ae1d5c8d9246653e0d93bc83abce0ee09ef725c0f0a29a41269c217b83.js", {},
160
+ 'HTTP_IF_NONE_MATCH' => '"b452c9ae1d5c8d9246653e0d93bc83abce0ee09ef725c0f0a29a41269c217b83"'
161
+ assert_equal 404, last_response.status
162
+ end
163
+
164
+ test "not found with response with incorrect fingerprint and matching if-none-match etags" do
165
+ get "/assets/foo.js"
166
+ assert_equal 200, last_response.status
167
+
168
+ etag = last_response.headers['ETag']
169
+
170
+ get "/assets/foo-0000000000000000000000000000000000000000.js", {},
171
+ 'HTTP_IF_NONE_MATCH' => etag
172
+ assert_equal 404, last_response.status
173
+ end
174
+
175
+ test "ok partial response when if-match etags match" do
176
+ get "/assets/foo.js"
177
+ assert_equal 200, last_response.status
178
+ etag = last_response.headers['ETag']
179
+
180
+ get "/assets/foo.js", {},
181
+ 'HTTP_IF_MATCH' => etag
182
+
183
+ assert_equal 200, last_response.status
184
+ assert_equal '"85b4185243c9cf4935e686f910a410762c2632a044118ac7ef030094be635c18"', last_response.headers['ETag']
185
+ assert_equal '53', last_response.headers['Content-Length']
186
+ end
187
+
188
+ test "precondition failed with if-match is a mismatch" do
189
+ get "/assets/foo.js", {},
190
+ 'HTTP_IF_MATCH' => '"000"'
191
+ assert_equal 412, last_response.status
192
+
193
+ refute last_response.headers['ETag']
194
+ end
195
+
196
+ test "not found with if-match" do
197
+ get "/assets/missing.js", {},
198
+ 'HTTP_IF_MATCH' => '"000"'
199
+ assert_equal 404, last_response.status
200
+ end
201
+
202
+ # TODO:
203
+ # test "if sources didnt change the server shouldnt rebundle" do
204
+ # get "/assets/application.js"
205
+ # asset_before = @env["application.js"]
206
+ # assert asset_before
207
+ #
208
+ # get "/assets/application.js"
209
+ # asset_after = @env["application.js"]
210
+ # assert asset_after
211
+ #
212
+ # assert asset_before.eql?(asset_after)
213
+ # end
214
+ #
215
+ test "fingerprint digest sets expiration to the future" do
216
+ get "/assets/foo.js"
217
+ digest = last_response.headers['ETag'][/"(.+)"/, 1]
218
+
219
+ get "/assets/foo-#{digest}.js"
220
+ assert_equal 200, last_response.status
221
+ assert_match %r{max-age}, last_response.headers['Cache-Control']
222
+ assert_match %r{immutable}, last_response.headers['Cache-Control']
223
+ end
224
+
225
+ test "bad fingerprint digest returns a 404" do
226
+ get "/assets/foo-0000000000000000000000000000000000000000.js"
227
+ assert_equal 404, last_response.status
228
+
229
+ head "/assets/foo-0000000000000000000000000000000000000000.js"
230
+ assert_equal 404, last_response.status
231
+ assert_equal "0", last_response.headers['Content-Length']
232
+ assert_equal "", last_response.body
233
+ end
234
+
235
+ test "missing source" do
236
+ get "/assets/none.js"
237
+ assert_equal 404, last_response.status
238
+ assert_equal "pass", last_response.headers['X-Cascade']
239
+ end
240
+
241
+ test "re-throw JS exceptions in the browser" do
242
+ file 'error.js', "var error = {;"
243
+
244
+ get "/assets/error.js"
245
+ assert_equal 200, last_response.status
246
+ assert_match(/SyntaxError: error\.js: Unexpected token/, last_response.body)
247
+ end
248
+
249
+ test "display CSS exceptions in the browser" do
250
+ file 'error.scss', "* { color: $test; }"
251
+
252
+ get "/assets/error.css"
253
+ assert_equal 200, last_response.status
254
+ assert_match %r{content: ".*?Sass::SyntaxError}, last_response.body
255
+ end
256
+
257
+ test "serve encoded utf-8 filename" do
258
+ file '日本語.js', <<~JS
259
+ var japanese = "日本語";
260
+
261
+ console.log(japanese);
262
+ JS
263
+ get "/assets/%E6%97%A5%E6%9C%AC%E8%AA%9E.js"
264
+ assert_equal <<~JS, last_response.body
265
+ (function () {
266
+ 'use strict';
267
+
268
+ var japanese = "日本語";
269
+
270
+ console.log(japanese);
271
+
272
+ }());
273
+ JS
274
+ end
275
+
276
+ test "illegal require outside load path" do
277
+ get "/assets//etc/passwd"
278
+ assert_equal 403, last_response.status
279
+
280
+ get "/assets/%2fetc/passwd"
281
+ assert_equal 403, last_response.status
282
+
283
+ get "/assets//%2fetc/passwd"
284
+ assert_equal 403, last_response.status
285
+
286
+ get "/assets/%2f/etc/passwd"
287
+ assert_equal 403, last_response.status
288
+
289
+ get "/assets/../etc/passwd"
290
+ assert_equal 403, last_response.status
291
+
292
+ get "/assets/%2e%2e/etc/passwd"
293
+ assert_equal 403, last_response.status
294
+
295
+ get "/assets/.-0000000./etc/passwd"
296
+ assert_equal 403, last_response.status
297
+
298
+ head "/assets/.-0000000./etc/passwd"
299
+ assert_equal 403, last_response.status
300
+ assert_equal "0", last_response.headers['Content-Length']
301
+ assert_equal "", last_response.body
302
+ end
303
+
304
+ # TODO:
305
+ # test "add new source to tree" do
306
+ # filename = fixture_path("server/app/javascripts/baz.js")
307
+ #
308
+ # sandbox filename do
309
+ # get "/assets/tree.js"
310
+ # assert_equal "var foo;\n\n(function () {\n application.boot();\n})();\nvar bar;\nvar japanese = \"日本語\";\n", last_response.body
311
+ #
312
+ # File.open(filename, "w") do |f|
313
+ # f.write "var baz;\n"
314
+ # end
315
+ #
316
+ # path = fixture_path "server/app/javascripts"
317
+ # mtime = Time.now + 60
318
+ # File.utime(mtime, mtime, path)
319
+ #
320
+ # get "/assets/tree.js"
321
+ # assert_equal "var foo;\n\n(function () {\n application.boot();\n})();\nvar bar;\nvar baz;\nvar japanese = \"日本語\";\n", last_response.body
322
+ # end
323
+ # end
324
+
325
+ test "serving static assets" do
326
+ bytes = Random.new.bytes(128)
327
+ file 'logo.png', bytes
328
+
329
+ get "/assets/logo.png"
330
+ assert_equal 200, last_response.status
331
+ assert_equal "image/png", last_response.headers['Content-Type']
332
+ refute last_response.headers['Content-Encoding']
333
+ assert_equal bytes, last_response.body
334
+ end
335
+
336
+ test "disallow non-get methods" do
337
+ get "/assets/foo.js"
338
+ assert_equal 200, last_response.status
339
+
340
+ head "/assets/foo.js"
341
+ assert_equal 200, last_response.status
342
+ assert_equal "application/javascript", last_response.headers['Content-Type']
343
+ assert_equal "0", last_response.headers['Content-Length']
344
+ assert_equal "", last_response.body
345
+
346
+ post "/assets/foo.js"
347
+ assert_equal 405, last_response.status
348
+
349
+ put "/assets/foo.js"
350
+ assert_equal 405, last_response.status
351
+
352
+ delete "/assets/foo.js"
353
+ assert_equal 405, last_response.status
354
+ end
355
+
356
+ test "invalid URLs" do
357
+ get "/assets/%E2%EF%BF%BD%A6.js"
358
+ assert_equal 400, last_response.status
359
+ end
360
+
361
+ end