secure_headers 3.0.3 → 3.1.2
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.
- checksums.yaml +4 -4
- data/.gitignore +0 -9
- data/.travis.yml +13 -5
- data/CHANGELOG.md +27 -0
- data/Gemfile +5 -2
- data/README.md +10 -42
- data/lib/secure_headers/configuration.rb +94 -54
- data/lib/secure_headers/headers/content_security_policy.rb +31 -309
- data/lib/secure_headers/headers/policy_management.rb +320 -0
- data/lib/secure_headers/headers/x_content_type_options.rb +1 -1
- data/lib/secure_headers/middleware.rb +23 -0
- data/lib/secure_headers/railtie.rb +1 -1
- data/lib/secure_headers.rb +37 -63
- data/secure_headers.gemspec +1 -1
- data/spec/lib/secure_headers/configuration_spec.rb +10 -4
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +0 -175
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +190 -0
- data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +1 -1
- data/spec/lib/secure_headers/middleware_spec.rb +23 -4
- data/spec/lib/secure_headers_spec.rb +119 -42
- data/spec/spec_helper.rb +4 -1
- metadata +5 -4
- data/lib/secure_headers/padrino.rb +0 -13
- data/travis.sh +0 -10
|
@@ -2,11 +2,11 @@ require "spec_helper"
|
|
|
2
2
|
|
|
3
3
|
module SecureHeaders
|
|
4
4
|
describe Middleware do
|
|
5
|
-
let(:app) {
|
|
5
|
+
let(:app) { lambda { |env| [200, env, "app"] } }
|
|
6
|
+
let(:cookie_app) { lambda { |env| [200, env.merge("Set-Cookie" => "foo=bar"), "app"] } }
|
|
6
7
|
|
|
7
|
-
let
|
|
8
|
-
|
|
9
|
-
end
|
|
8
|
+
let(:middleware) { Middleware.new(app) }
|
|
9
|
+
let(:cookie_middleware) { Middleware.new(cookie_app) }
|
|
10
10
|
|
|
11
11
|
before(:each) do
|
|
12
12
|
reset_config
|
|
@@ -33,8 +33,27 @@ module SecureHeaders
|
|
|
33
33
|
end
|
|
34
34
|
request = Rack::Request.new({})
|
|
35
35
|
SecureHeaders.use_secure_headers_override(request, "my_custom_config")
|
|
36
|
+
expect(request.env[SECURE_HEADERS_CONFIG]).to be(Configuration.get("my_custom_config"))
|
|
36
37
|
_, env = middleware.call request.env
|
|
37
38
|
expect(env[CSP::HEADER_NAME]).to match("example.org")
|
|
38
39
|
end
|
|
40
|
+
|
|
41
|
+
context "cookies should be flagged" do
|
|
42
|
+
it "flags cookies as secure" do
|
|
43
|
+
Configuration.default { |config| config.secure_cookies = true }
|
|
44
|
+
request = Rack::MockRequest.new(cookie_middleware)
|
|
45
|
+
response = request.get '/'
|
|
46
|
+
expect(response.headers['Set-Cookie']).to match(Middleware::SECURE_COOKIE_REGEXP)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
context "cookies should not be flagged" do
|
|
51
|
+
it "does not flags cookies as secure" do
|
|
52
|
+
Configuration.default { |config| config.secure_cookies = false }
|
|
53
|
+
request = Rack::MockRequest.new(cookie_middleware)
|
|
54
|
+
response = request.get '/'
|
|
55
|
+
expect(response.headers['Set-Cookie']).not_to match(Middleware::SECURE_COOKIE_REGEXP)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
39
58
|
end
|
|
40
59
|
end
|
|
@@ -2,57 +2,66 @@ require 'spec_helper'
|
|
|
2
2
|
|
|
3
3
|
module SecureHeaders
|
|
4
4
|
describe SecureHeaders do
|
|
5
|
-
example_hpkp_config = {
|
|
6
|
-
max_age: 1_000_000,
|
|
7
|
-
include_subdomains: true,
|
|
8
|
-
report_uri: '//example.com/uri-directive',
|
|
9
|
-
pins: [
|
|
10
|
-
{ sha256: 'abc' },
|
|
11
|
-
{ sha256: '123' }
|
|
12
|
-
]
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
example_hpkp_config_value = %(max-age=1000000; pin-sha256="abc"; pin-sha256="123"; report-uri="//example.com/uri-directive"; includeSubDomains)
|
|
16
|
-
|
|
17
5
|
before(:each) do
|
|
18
6
|
reset_config
|
|
19
|
-
@request = Rack::Request.new("HTTP_X_FORWARDED_SSL" => "on")
|
|
20
7
|
end
|
|
21
8
|
|
|
9
|
+
let(:request) { Rack::Request.new("HTTP_X_FORWARDED_SSL" => "on") }
|
|
10
|
+
|
|
22
11
|
it "raises a NotYetConfiguredError if default has not been set" do
|
|
23
12
|
expect do
|
|
24
|
-
SecureHeaders.header_hash_for(
|
|
13
|
+
SecureHeaders.header_hash_for(request)
|
|
25
14
|
end.to raise_error(Configuration::NotYetConfiguredError)
|
|
26
15
|
end
|
|
27
16
|
|
|
28
17
|
it "raises a NotYetConfiguredError if trying to opt-out of unconfigured headers" do
|
|
29
18
|
expect do
|
|
30
|
-
SecureHeaders.opt_out_of_header(
|
|
19
|
+
SecureHeaders.opt_out_of_header(request, CSP::CONFIG_KEY)
|
|
31
20
|
end.to raise_error(Configuration::NotYetConfiguredError)
|
|
32
21
|
end
|
|
33
22
|
|
|
34
23
|
describe "#header_hash_for" do
|
|
35
|
-
it "allows you to opt out of individual headers" do
|
|
24
|
+
it "allows you to opt out of individual headers via API" do
|
|
36
25
|
Configuration.default
|
|
37
|
-
SecureHeaders.opt_out_of_header(
|
|
38
|
-
|
|
26
|
+
SecureHeaders.opt_out_of_header(request, CSP::CONFIG_KEY)
|
|
27
|
+
SecureHeaders.opt_out_of_header(request, XContentTypeOptions::CONFIG_KEY)
|
|
28
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
39
29
|
expect(hash['Content-Security-Policy-Report-Only']).to be_nil
|
|
40
30
|
expect(hash['Content-Security-Policy']).to be_nil
|
|
31
|
+
expect(hash['X-Content-Type-Options']).to be_nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "Carries options over when using overrides" do
|
|
35
|
+
Configuration.default do |config|
|
|
36
|
+
config.x_download_options = OPT_OUT
|
|
37
|
+
config.x_permitted_cross_domain_policies = OPT_OUT
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
Configuration.override(:api) do |config|
|
|
41
|
+
config.x_frame_options = OPT_OUT
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
SecureHeaders.use_secure_headers_override(request, :api)
|
|
45
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
46
|
+
expect(hash['X-Download-Options']).to be_nil
|
|
47
|
+
expect(hash['X-Permitted-Cross-Domain-Policies']).to be_nil
|
|
48
|
+
expect(hash['X-Frame-Options']).to be_nil
|
|
41
49
|
end
|
|
42
50
|
|
|
43
51
|
it "allows you to opt out entirely" do
|
|
44
52
|
Configuration.default
|
|
45
|
-
SecureHeaders.opt_out_of_all_protection(
|
|
46
|
-
hash = SecureHeaders.header_hash_for(
|
|
53
|
+
SecureHeaders.opt_out_of_all_protection(request)
|
|
54
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
47
55
|
ALL_HEADER_CLASSES.each do |klass|
|
|
48
56
|
expect(hash[klass::CONFIG_KEY]).to be_nil
|
|
49
57
|
end
|
|
58
|
+
expect(hash.count).to eq(0)
|
|
50
59
|
end
|
|
51
60
|
|
|
52
61
|
it "allows you to override X-Frame-Options settings" do
|
|
53
62
|
Configuration.default
|
|
54
|
-
SecureHeaders.override_x_frame_options(
|
|
55
|
-
hash = SecureHeaders.header_hash_for(
|
|
63
|
+
SecureHeaders.override_x_frame_options(request, XFrameOptions::DENY)
|
|
64
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
56
65
|
expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::DENY)
|
|
57
66
|
end
|
|
58
67
|
|
|
@@ -62,17 +71,36 @@ module SecureHeaders
|
|
|
62
71
|
config.csp = OPT_OUT
|
|
63
72
|
end
|
|
64
73
|
|
|
65
|
-
SecureHeaders.override_x_frame_options(
|
|
66
|
-
SecureHeaders.override_content_security_policy_directives(
|
|
74
|
+
SecureHeaders.override_x_frame_options(request, XFrameOptions::SAMEORIGIN)
|
|
75
|
+
SecureHeaders.override_content_security_policy_directives(request, default_src: %w(https:), script_src: %w('self'))
|
|
67
76
|
|
|
68
|
-
hash = SecureHeaders.header_hash_for(
|
|
77
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
69
78
|
expect(hash[CSP::HEADER_NAME]).to eq("default-src https:; script-src 'self'")
|
|
70
79
|
expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::SAMEORIGIN)
|
|
71
80
|
end
|
|
72
81
|
|
|
82
|
+
it "produces a UA-specific CSP when overriding (and busting the cache)" do
|
|
83
|
+
config = Configuration.default do |config|
|
|
84
|
+
config.csp = {
|
|
85
|
+
default_src: %w('self'),
|
|
86
|
+
child_src: %w('self'), #unsupported by firefox
|
|
87
|
+
frame_src: %w('self')
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
firefox_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:firefox]))
|
|
91
|
+
|
|
92
|
+
# append an unsupported directive
|
|
93
|
+
SecureHeaders.override_content_security_policy_directives(firefox_request, plugin_types: %w(flash))
|
|
94
|
+
# append a supported directive
|
|
95
|
+
SecureHeaders.override_content_security_policy_directives(firefox_request, script_src: %w('self'))
|
|
96
|
+
|
|
97
|
+
hash = SecureHeaders.header_hash_for(firefox_request)
|
|
98
|
+
expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; frame-src 'self'; script-src 'self'")
|
|
99
|
+
end
|
|
100
|
+
|
|
73
101
|
it "produces a hash of headers with default config" do
|
|
74
102
|
Configuration.default
|
|
75
|
-
hash = SecureHeaders.header_hash_for(
|
|
103
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
76
104
|
expect_default_values(hash)
|
|
77
105
|
end
|
|
78
106
|
|
|
@@ -87,7 +115,15 @@ module SecureHeaders
|
|
|
87
115
|
it "does not set the HPKP header if request is over HTTP" do
|
|
88
116
|
plaintext_request = Rack::Request.new({})
|
|
89
117
|
Configuration.default do |config|
|
|
90
|
-
config.hpkp =
|
|
118
|
+
config.hpkp = {
|
|
119
|
+
max_age: 1_000_000,
|
|
120
|
+
include_subdomains: true,
|
|
121
|
+
report_uri: '//example.com/uri-directive',
|
|
122
|
+
pins: [
|
|
123
|
+
{ sha256: 'abc' },
|
|
124
|
+
{ sha256: '123' }
|
|
125
|
+
]
|
|
126
|
+
}
|
|
91
127
|
end
|
|
92
128
|
|
|
93
129
|
expect(SecureHeaders.header_hash_for(plaintext_request)[PublicKeyPins::HEADER_NAME]).to be_nil
|
|
@@ -102,26 +138,67 @@ module SecureHeaders
|
|
|
102
138
|
}
|
|
103
139
|
end
|
|
104
140
|
|
|
105
|
-
SecureHeaders.append_content_security_policy_directives(
|
|
106
|
-
hash = SecureHeaders.header_hash_for(
|
|
141
|
+
SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com))
|
|
142
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
107
143
|
expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com")
|
|
108
144
|
end
|
|
109
145
|
|
|
146
|
+
it "dups global configuration just once when overriding n times and only calls idempotent_additions? once" do
|
|
147
|
+
Configuration.default do |config|
|
|
148
|
+
config.csp = {
|
|
149
|
+
default_src: %w('self')
|
|
150
|
+
}
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
expect(CSP).to receive(:idempotent_additions?).once
|
|
154
|
+
|
|
155
|
+
# before an override occurs, the env is empty
|
|
156
|
+
expect(request.env[SECURE_HEADERS_CONFIG]).to be_nil
|
|
157
|
+
|
|
158
|
+
SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com))
|
|
159
|
+
new_config = SecureHeaders.config_for(request)
|
|
160
|
+
expect(new_config).to_not be(Configuration.get)
|
|
161
|
+
|
|
162
|
+
SecureHeaders.override_content_security_policy_directives(request, script_src: %w(yet.anothercdn.com))
|
|
163
|
+
current_config = SecureHeaders.config_for(request)
|
|
164
|
+
expect(current_config).to be(new_config)
|
|
165
|
+
|
|
166
|
+
SecureHeaders.header_hash_for(request)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it "doesn't allow you to muck with csp configs when a dynamic policy is in use" do
|
|
170
|
+
default_config = Configuration.default
|
|
171
|
+
expect { default_config.csp = {} }.to raise_error(NoMethodError)
|
|
172
|
+
|
|
173
|
+
# config is frozen
|
|
174
|
+
expect { default_config.send(:csp=, {}) }.to raise_error(RuntimeError)
|
|
175
|
+
|
|
176
|
+
SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com))
|
|
177
|
+
new_config = SecureHeaders.config_for(request)
|
|
178
|
+
expect { new_config.send(:csp=, {}) }.to raise_error(Configuration::IllegalPolicyModificationError)
|
|
179
|
+
|
|
180
|
+
expect do
|
|
181
|
+
new_config.instance_eval do
|
|
182
|
+
new_config.csp = {}
|
|
183
|
+
end
|
|
184
|
+
end.to raise_error(Configuration::IllegalPolicyModificationError)
|
|
185
|
+
end
|
|
186
|
+
|
|
110
187
|
it "overrides individual directives" do
|
|
111
188
|
Configuration.default do |config|
|
|
112
189
|
config.csp = {
|
|
113
190
|
default_src: %w('self')
|
|
114
191
|
}
|
|
115
192
|
end
|
|
116
|
-
SecureHeaders.override_content_security_policy_directives(
|
|
117
|
-
hash = SecureHeaders.header_hash_for(
|
|
193
|
+
SecureHeaders.override_content_security_policy_directives(request, default_src: %w('none'))
|
|
194
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
118
195
|
expect(hash[CSP::HEADER_NAME]).to eq("default-src 'none'")
|
|
119
196
|
end
|
|
120
197
|
|
|
121
198
|
it "overrides non-existant directives" do
|
|
122
199
|
Configuration.default
|
|
123
|
-
SecureHeaders.override_content_security_policy_directives(
|
|
124
|
-
hash = SecureHeaders.header_hash_for(
|
|
200
|
+
SecureHeaders.override_content_security_policy_directives(request, img_src: [ContentSecurityPolicy::DATA_PROTOCOL])
|
|
201
|
+
hash = SecureHeaders.header_hash_for(request)
|
|
125
202
|
expect(hash[CSP::HEADER_NAME]).to eq("default-src https:; img-src data:")
|
|
126
203
|
end
|
|
127
204
|
|
|
@@ -134,9 +211,9 @@ module SecureHeaders
|
|
|
134
211
|
}
|
|
135
212
|
end
|
|
136
213
|
|
|
137
|
-
|
|
138
|
-
nonce = SecureHeaders.content_security_policy_script_nonce(
|
|
139
|
-
hash = SecureHeaders.header_hash_for(
|
|
214
|
+
safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari5]))
|
|
215
|
+
nonce = SecureHeaders.content_security_policy_script_nonce(safari_request)
|
|
216
|
+
hash = SecureHeaders.header_hash_for(safari_request)
|
|
140
217
|
expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline'; style-src 'self'")
|
|
141
218
|
end
|
|
142
219
|
|
|
@@ -149,15 +226,15 @@ module SecureHeaders
|
|
|
149
226
|
}
|
|
150
227
|
end
|
|
151
228
|
|
|
152
|
-
|
|
153
|
-
nonce = SecureHeaders.content_security_policy_script_nonce(
|
|
229
|
+
chrome_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:chrome]))
|
|
230
|
+
nonce = SecureHeaders.content_security_policy_script_nonce(chrome_request)
|
|
154
231
|
|
|
155
232
|
# simulate the nonce being used multiple times in a request:
|
|
156
|
-
SecureHeaders.content_security_policy_script_nonce(
|
|
157
|
-
SecureHeaders.content_security_policy_script_nonce(
|
|
158
|
-
SecureHeaders.content_security_policy_script_nonce(
|
|
233
|
+
SecureHeaders.content_security_policy_script_nonce(chrome_request)
|
|
234
|
+
SecureHeaders.content_security_policy_script_nonce(chrome_request)
|
|
235
|
+
SecureHeaders.content_security_policy_script_nonce(chrome_request)
|
|
159
236
|
|
|
160
|
-
hash = SecureHeaders.header_hash_for(
|
|
237
|
+
hash = SecureHeaders.header_hash_for(chrome_request)
|
|
161
238
|
expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'; style-src 'self'")
|
|
162
239
|
end
|
|
163
240
|
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -3,9 +3,12 @@ require 'rspec'
|
|
|
3
3
|
require 'rack'
|
|
4
4
|
require 'pry-nav'
|
|
5
5
|
|
|
6
|
+
require 'coveralls'
|
|
7
|
+
Coveralls.wear!
|
|
8
|
+
|
|
6
9
|
require File.join(File.dirname(__FILE__), '..', 'lib', 'secure_headers')
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
|
|
9
12
|
|
|
10
13
|
USER_AGENTS = {
|
|
11
14
|
firefox: 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1',
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: secure_headers
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Neil Matatall
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2016-
|
|
11
|
+
date: 2016-03-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|
|
@@ -59,6 +59,7 @@ files:
|
|
|
59
59
|
- lib/secure_headers.rb
|
|
60
60
|
- lib/secure_headers/configuration.rb
|
|
61
61
|
- lib/secure_headers/headers/content_security_policy.rb
|
|
62
|
+
- lib/secure_headers/headers/policy_management.rb
|
|
62
63
|
- lib/secure_headers/headers/public_key_pins.rb
|
|
63
64
|
- lib/secure_headers/headers/strict_transport_security.rb
|
|
64
65
|
- lib/secure_headers/headers/x_content_type_options.rb
|
|
@@ -67,12 +68,12 @@ files:
|
|
|
67
68
|
- lib/secure_headers/headers/x_permitted_cross_domain_policies.rb
|
|
68
69
|
- lib/secure_headers/headers/x_xss_protection.rb
|
|
69
70
|
- lib/secure_headers/middleware.rb
|
|
70
|
-
- lib/secure_headers/padrino.rb
|
|
71
71
|
- lib/secure_headers/railtie.rb
|
|
72
72
|
- lib/secure_headers/view_helper.rb
|
|
73
73
|
- secure_headers.gemspec
|
|
74
74
|
- spec/lib/secure_headers/configuration_spec.rb
|
|
75
75
|
- spec/lib/secure_headers/headers/content_security_policy_spec.rb
|
|
76
|
+
- spec/lib/secure_headers/headers/policy_management_spec.rb
|
|
76
77
|
- spec/lib/secure_headers/headers/public_key_pins_spec.rb
|
|
77
78
|
- spec/lib/secure_headers/headers/strict_transport_security_spec.rb
|
|
78
79
|
- spec/lib/secure_headers/headers/x_content_type_options_spec.rb
|
|
@@ -83,7 +84,6 @@ files:
|
|
|
83
84
|
- spec/lib/secure_headers/middleware_spec.rb
|
|
84
85
|
- spec/lib/secure_headers_spec.rb
|
|
85
86
|
- spec/spec_helper.rb
|
|
86
|
-
- travis.sh
|
|
87
87
|
- upgrading-to-3-0.md
|
|
88
88
|
homepage: https://github.com/twitter/secureheaders
|
|
89
89
|
licenses:
|
|
@@ -113,6 +113,7 @@ summary: Add easily configured security headers to responses including content-s
|
|
|
113
113
|
test_files:
|
|
114
114
|
- spec/lib/secure_headers/configuration_spec.rb
|
|
115
115
|
- spec/lib/secure_headers/headers/content_security_policy_spec.rb
|
|
116
|
+
- spec/lib/secure_headers/headers/policy_management_spec.rb
|
|
116
117
|
- spec/lib/secure_headers/headers/public_key_pins_spec.rb
|
|
117
118
|
- spec/lib/secure_headers/headers/strict_transport_security_spec.rb
|
|
118
119
|
- spec/lib/secure_headers/headers/x_content_type_options_spec.rb
|
data/travis.sh
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
#! /bin/sh
|
|
2
|
-
|
|
3
|
-
bundle install >> /dev/null &&
|
|
4
|
-
bundle exec rspec --format progress spec &&
|
|
5
|
-
cd fixtures/rails_3_2_12 &&
|
|
6
|
-
bundle install >> /dev/null &&
|
|
7
|
-
bundle exec rspec --format progress spec &&
|
|
8
|
-
cd ../../fixtures/rails_3_2_12_no_init &&
|
|
9
|
-
bundle install >> /dev/null &&
|
|
10
|
-
bundle exec rspec spec
|