flipper-ui 0.21.0.rc1 → 0.22.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/docs/ui/README.md +1 -1
  3. data/examples/ui/authorization.ru +12 -50
  4. data/examples/ui/basic.ru +12 -23
  5. data/lib/flipper/ui/action.rb +48 -1
  6. data/lib/flipper/ui/actions/actors_gate.rb +1 -1
  7. data/lib/flipper/ui/actions/features.rb +2 -2
  8. data/lib/flipper/ui/actions/file.rb +1 -1
  9. data/lib/flipper/ui/actions/groups_gate.rb +1 -1
  10. data/lib/flipper/ui/actions/percentage_of_actors_gate.rb +1 -1
  11. data/lib/flipper/ui/actions/percentage_of_time_gate.rb +1 -1
  12. data/lib/flipper/ui/decorators/feature.rb +3 -3
  13. data/lib/flipper/ui/middleware.rb +2 -1
  14. data/lib/flipper/ui/public/css/application.css +7 -0
  15. data/lib/flipper/ui/views/feature.erb +3 -3
  16. data/lib/flipper/ui/views/features.erb +1 -1
  17. data/lib/flipper/ui/views/layout.erb +4 -5
  18. data/lib/flipper/ui.rb +2 -2
  19. data/lib/flipper/version.rb +1 -1
  20. data/spec/flipper/ui/actions/actors_gate_spec.rb +19 -2
  21. data/spec/flipper/ui/actions/boolean_gate_spec.rb +18 -0
  22. data/spec/flipper/ui/actions/feature_spec.rb +18 -0
  23. data/spec/flipper/ui/actions/features_spec.rb +16 -3
  24. data/spec/flipper/ui/actions/file_spec.rb +0 -10
  25. data/spec/flipper/ui/actions/groups_gate_spec.rb +20 -3
  26. data/spec/flipper/ui/actions/percentage_of_actors_gate_spec.rb +18 -1
  27. data/spec/flipper/ui/actions/percentage_of_time_gate_spec.rb +18 -1
  28. data/spec/flipper/ui_spec.rb +0 -13
  29. metadata +6 -14
  30. data/lib/flipper/ui/public/octicons/LICENSE.txt +0 -9
  31. data/lib/flipper/ui/public/octicons/README.md +0 -1
  32. data/lib/flipper/ui/public/octicons/octicons-local.ttf +0 -0
  33. data/lib/flipper/ui/public/octicons/octicons.css +0 -236
  34. data/lib/flipper/ui/public/octicons/octicons.eot +0 -0
  35. data/lib/flipper/ui/public/octicons/octicons.svg +0 -200
  36. data/lib/flipper/ui/public/octicons/octicons.ttf +0 -0
  37. data/lib/flipper/ui/public/octicons/octicons.woff +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1892ab407f3f56ccddb2268e6ce53d7d02ef68a5004c914bc68a61c791c1a97f
4
- data.tar.gz: cff24cf7ed9f5521118d16af76cd2b8bc9b7ef90b345610450f2c2d836cc915d
3
+ metadata.gz: a619370e297b8fc379c57c8394cdfc798120ebfd9d4d091bc1902f3d9a2183be
4
+ data.tar.gz: a36c5d0a79836aec62188c04f5954a506d737e110a135028b3fc7d9d889a2579
5
5
  SHA512:
6
- metadata.gz: 1c3afd4b54efb3d6df85b1be0df6671083d208a451526a71483519eb748ff87aaf4b5ca1d4d4594981b6172fbb7e4b82e6e7cab3268d26c237f98da499c3bf18
7
- data.tar.gz: fc85ea7b8bae97535e34321b1c89ae1dcda2b834dc31876d91ddc7ec5387d52a2f912c53c1cf8a965c3442e655676923ee1040385fe3bd73df4b8c2a82af4c04
6
+ metadata.gz: b02d0683b248297b59e6013f816a6f09bbde28889e38201e0f41c89d29118f06f16061749d55041a786daf81d2d67da8e88885033c4819233794dbd55df3ea8d
7
+ data.tar.gz: 425e485bc419c1ae45554298df69a2d6cfdb4d2169fa2b0dc1b3e5e32009e5bafd27372796ab8a1bf3ac68dbc2d1be6a2474cbd7855b1d245fd61c8940e1cb81
data/docs/ui/README.md CHANGED
@@ -109,7 +109,7 @@ Minimal example for Rack:
109
109
  ```ruby
110
110
  # config.ru
111
111
 
112
- require 'flipper-ui'
112
+ require 'flipper/ui'
113
113
 
114
114
  adapter = Flipper::Adapters::Memory.new
115
115
  flipper = Flipper.new(adapter)
@@ -5,50 +5,13 @@
5
5
  # http://localhost:9999/
6
6
  #
7
7
  require 'bundler/setup'
8
- require "logger"
9
-
10
- require "flipper-ui"
8
+ require "flipper/ui"
11
9
  require "flipper/adapters/pstore"
12
- require "active_support/notifications"
13
10
 
14
11
  Flipper.register(:admins) { |actor|
15
12
  actor.respond_to?(:admin?) && actor.admin?
16
13
  }
17
14
 
18
- Flipper.register(:early_access) { |actor|
19
- actor.respond_to?(:early?) && actor.early?
20
- }
21
-
22
- # Setup logging of flipper calls.
23
- if ENV["LOG"] == "1"
24
- $logger = Logger.new(STDOUT)
25
- require "flipper/instrumentation/log_subscriber"
26
- Flipper::Instrumentation::LogSubscriber.logger = $logger
27
- end
28
-
29
- adapter = Flipper::Adapters::PStore.new
30
- flipper = Flipper.new(adapter, instrumenter: ActiveSupport::Notifications)
31
-
32
- Flipper::UI.configure do |config|
33
- # config.banner_text = 'Production Environment'
34
- # config.banner_class = 'danger'
35
- config.feature_creation_enabled = true
36
- config.feature_removal_enabled = true
37
- # config.show_feature_description_in_list = true
38
- config.descriptions_source = lambda do |_keys|
39
- {
40
- "search_performance_another_long_thing" => "Just to test feature name length.",
41
- "gauges_tracking" => "Should we track page views with gaug.es.",
42
- "unused" => "Not used.",
43
- "suits" => "Are suits necessary in business?",
44
- "secrets" => "Secrets are lies.",
45
- "logging" => "Log all the things.",
46
- "new_cache" => "Like the old cache but newer.",
47
- "a/b" => "Why would someone use a slash? I don't know but someone did. Let's make this really long so they regret using slashes. Please don't use slashes.",
48
- }
49
- end
50
- end
51
-
52
15
  # Example middleware to allow reading the Flipper UI but nothing else.
53
16
  class FlipperReadOnlyMiddleware
54
17
  def initialize(app)
@@ -67,18 +30,17 @@ class FlipperReadOnlyMiddleware
67
30
  end
68
31
 
69
32
  # You can uncomment these to get some default data:
70
- # flipper[:search_performance_another_long_thing].enable
71
- # flipper[:gauges_tracking].enable
72
- # flipper[:unused].disable
73
- # flipper[:suits].enable_actor Flipper::Actor.new('1')
74
- # flipper[:suits].enable_actor Flipper::Actor.new('6')
75
- # flipper[:secrets].enable_group :admins
76
- # flipper[:secrets].enable_group :early_access
77
- # flipper[:logging].enable_percentage_of_time 5
78
- # flipper[:new_cache].enable_percentage_of_actors 15
79
- # flipper["something/slashed"].add
80
-
81
- run Flipper::UI.app(flipper) { |builder|
33
+ # Flipper.enable(:search_performance_another_long_thing)
34
+ # Flipper.disable(:gauges_tracking)
35
+ # Flipper.disable(:unused)
36
+ # Flipper.enable_actor(:suits, Flipper::Actor.new('1'))
37
+ # Flipper.enable_actor(:suits, Flipper::Actor.new('6'))
38
+ # Flipper.enable_group(:secrets, :admins)
39
+ # Flipper.enable_percentage_of_time(:logging, 5)
40
+ # Flipper.enable_percentage_of_actors(:new_cache, 15)
41
+ # Flipper.add("a/b")
42
+
43
+ run Flipper::UI.app { |builder|
82
44
  builder.use Rack::Session::Cookie, secret: "_super_secret"
83
45
  builder.use FlipperReadOnlyMiddleware
84
46
  }
data/examples/ui/basic.ru CHANGED
@@ -9,9 +9,8 @@
9
9
  # http://localhost:9999/
10
10
  #
11
11
  require 'bundler/setup'
12
- require "flipper-ui"
12
+ require "flipper/ui"
13
13
  require "flipper/adapters/pstore"
14
- require "active_support/notifications"
15
14
 
16
15
  Flipper.register(:admins) { |actor|
17
16
  actor.respond_to?(:admin?) && actor.admin?
@@ -21,16 +20,6 @@ Flipper.register(:early_access) { |actor|
21
20
  actor.respond_to?(:early?) && actor.early?
22
21
  }
23
22
 
24
- # Setup logging of flipper calls.
25
- if ENV["LOG"] == "1"
26
- $logger = Logger.new(STDOUT)
27
- require "flipper/instrumentation/log_subscriber"
28
- Flipper::Instrumentation::LogSubscriber.logger = $logger
29
- end
30
-
31
- adapter = Flipper::Adapters::PStore.new
32
- flipper = Flipper.new(adapter, instrumenter: ActiveSupport::Notifications)
33
-
34
23
  Flipper::UI.configure do |config|
35
24
  # config.banner_text = 'Production Environment'
36
25
  # config.banner_class = 'danger'
@@ -53,17 +42,17 @@ Flipper::UI.configure do |config|
53
42
  end
54
43
 
55
44
  # You can uncomment these to get some default data:
56
- # flipper[:search_performance_another_long_thing].enable
57
- # flipper[:gauges_tracking].enable
58
- # flipper[:unused].disable
59
- # flipper[:suits].enable_actor Flipper::Actor.new('1')
60
- # flipper[:suits].enable_actor Flipper::Actor.new('6')
61
- # flipper[:secrets].enable_group :admins
62
- # flipper[:secrets].enable_group :early_access
63
- # flipper[:logging].enable_percentage_of_time 5
64
- # flipper[:new_cache].enable_percentage_of_actors 15
65
- # flipper["a/b"].add
45
+ # Flipper.enable(:search_performance_another_long_thing)
46
+ # Flipper.disable(:gauges_tracking)
47
+ # Flipper.disable(:unused)
48
+ # Flipper.enable_actor(:suits, Flipper::Actor.new('1'))
49
+ # Flipper.enable_actor(:suits, Flipper::Actor.new('6'))
50
+ # Flipper.enable_group(:secrets, :admins)
51
+ # Flipper.enable_group(:secrets, :early_access)
52
+ # Flipper.enable_percentage_of_time(:logging, 5)
53
+ # Flipper.enable_percentage_of_actors(:new_cache, 15)
54
+ # Flipper.add("a/b")
66
55
 
67
- run Flipper::UI.app(flipper) { |builder|
56
+ run Flipper::UI.app { |builder|
68
57
  builder.use Rack::Session::Cookie, secret: "_super_secret"
69
58
  }
@@ -26,6 +26,36 @@ module Flipper
26
26
  'delete'.freeze,
27
27
  ]).freeze
28
28
 
29
+ SOURCES = {
30
+ bootstrap_css: {
31
+ src: "https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css".freeze,
32
+ hash: "sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm".freeze
33
+ }.freeze,
34
+ jquery_js: {
35
+ src: "https://code.jquery.com/jquery-3.2.1.slim.min.js".freeze,
36
+ hash: "sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN".freeze
37
+ }.freeze,
38
+ popper_js: {
39
+ src: "https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js".freeze,
40
+ hash: "sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q".freeze
41
+ }.freeze,
42
+ bootstrap_js: {
43
+ src: "https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js".freeze,
44
+ hash: "sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl".freeze
45
+ }.freeze
46
+ }.freeze
47
+ SCRIPT_SRCS = SOURCES.values_at(:jquery_js, :popper_js, :bootstrap_js).map { |s| s[:src] }
48
+ STYLE_SRCS = SOURCES.values_at(:bootstrap_css).map { |s| s[:src] }
49
+ CONTENT_SECURITY_POLICY = <<-CSP.delete("\n")
50
+ default-src 'none';
51
+ img-src 'self';
52
+ font-src 'self';
53
+ script-src 'report-sample' 'self' #{SCRIPT_SRCS.join(' ')};
54
+ style-src 'self' 'unsafe-inline' #{STYLE_SRCS.join(' ')};
55
+ style-src-attr 'unsafe-inline' ;
56
+ style-src-elem 'self' #{STYLE_SRCS.join(' ')};
57
+ CSP
58
+
29
59
  # Public: Call this in subclasses so the action knows its route.
30
60
  #
31
61
  # regex - The Regexp that this action should run for.
@@ -130,6 +160,7 @@ module Flipper
130
160
  # Returns a response.
131
161
  def view_response(name)
132
162
  header 'Content-Type', 'text/html'
163
+ header 'Content-Security-Policy', CONTENT_SECURITY_POLICY
133
164
  body = view_with_layout { view_without_layout name }
134
165
  halt [@code, @headers, [body]]
135
166
  end
@@ -151,7 +182,7 @@ module Flipper
151
182
  # location - The String location to set the Location header to.
152
183
  def redirect_to(location)
153
184
  status 302
154
- header 'Location', "#{script_name}#{location}"
185
+ header 'Location', "#{script_name}#{Rack::Utils.escape_path(location)}"
155
186
  halt [@code, @headers, ['']]
156
187
  end
157
188
 
@@ -237,6 +268,22 @@ module Flipper
237
268
  def valid_request_method?
238
269
  VALID_REQUEST_METHOD_NAMES.include?(request_method_name)
239
270
  end
271
+
272
+ def bootstrap_css
273
+ SOURCES[:bootstrap_css]
274
+ end
275
+
276
+ def bootstrap_js
277
+ SOURCES[:bootstrap_js]
278
+ end
279
+
280
+ def popper_js
281
+ SOURCES[:popper_js]
282
+ end
283
+
284
+ def jquery_js
285
+ SOURCES[:jquery_js]
286
+ end
240
287
  end
241
288
  end
242
289
  end
@@ -27,7 +27,7 @@ module Flipper
27
27
  value = params['value'].to_s.strip
28
28
 
29
29
  if Util.blank?(value)
30
- error = Rack::Utils.escape("#{value.inspect} is not a valid actor value.")
30
+ error = "#{value.inspect} is not a valid actor value."
31
31
  redirect_to("/features/#{feature.key}/actors?error=#{error}")
32
32
  end
33
33
 
@@ -49,14 +49,14 @@ module Flipper
49
49
  value = params['value'].to_s.strip
50
50
 
51
51
  if Util.blank?(value)
52
- error = Rack::Utils.escape("#{value.inspect} is not a valid feature name.")
52
+ error = "#{value.inspect} is not a valid feature name."
53
53
  redirect_to("/features/new?error=#{error}")
54
54
  end
55
55
 
56
56
  feature = flipper[value]
57
57
  feature.add
58
58
 
59
- redirect_to "/features/#{Rack::Utils.escape_path(value)}"
59
+ redirect_to "/features/#{value}"
60
60
  end
61
61
  end
62
62
  end
@@ -5,7 +5,7 @@ module Flipper
5
5
  module UI
6
6
  module Actions
7
7
  class File < UI::Action
8
- route %r{(images|css|js|octicons)/.*\Z}
8
+ route %r{(images|css|js)/.*\Z}
9
9
 
10
10
  def get
11
11
  Rack::File.new(public_path).call(request.env)
@@ -35,7 +35,7 @@ module Flipper
35
35
 
36
36
  redirect_to("/features/#{feature.key}")
37
37
  else
38
- error = Rack::Utils.escape("The group named #{value.inspect} has not been registered.")
38
+ error = "The group named #{value.inspect} has not been registered."
39
39
  redirect_to("/features/#{feature.key}/groups?error=#{error}")
40
40
  end
41
41
  end
@@ -16,7 +16,7 @@ module Flipper
16
16
  begin
17
17
  feature.enable_percentage_of_actors params['value']
18
18
  rescue ArgumentError => exception
19
- error = Rack::Utils.escape("Invalid percentage of actors value: #{exception.message}")
19
+ error = "Invalid percentage of actors value: #{exception.message}"
20
20
  redirect_to("/features/#{@feature.key}?error=#{error}")
21
21
  end
22
22
 
@@ -16,7 +16,7 @@ module Flipper
16
16
  begin
17
17
  feature.enable_percentage_of_time params['value']
18
18
  rescue ArgumentError => exception
19
- error = Rack::Utils.escape("Invalid percentage of time value: #{exception.message}")
19
+ error = "Invalid percentage of time value: #{exception.message}"
20
20
  redirect_to("/features/#{@feature.key}?error=#{error}")
21
21
  end
22
22
 
@@ -23,11 +23,11 @@ module Flipper
23
23
  def color_class
24
24
  case feature.state
25
25
  when :on
26
- 'text-success'
26
+ 'bg-success'
27
27
  when :off
28
- 'text-danger'
28
+ 'bg-danger'
29
29
  when :conditional
30
- 'text-warning'
30
+ 'bg-warning'
31
31
  end
32
32
  end
33
33
 
@@ -12,6 +12,7 @@ module Flipper
12
12
  def initialize(app, options = {})
13
13
  @app = app
14
14
  @env_key = options.fetch(:env_key, 'flipper')
15
+ @flipper = options.fetch(:flipper) { Flipper }
15
16
 
16
17
  @action_collection = ActionCollection.new
17
18
 
@@ -43,7 +44,7 @@ module Flipper
43
44
  if action_class.nil?
44
45
  @app.call(env)
45
46
  else
46
- flipper = env.fetch(@env_key)
47
+ flipper = env.fetch(@env_key) { Flipper }
47
48
  action_class.run(flipper, request)
48
49
  end
49
50
  end
@@ -29,3 +29,10 @@ html {
29
29
  .toggle-on .toggle-block-when-off {
30
30
  display: none;
31
31
  }
32
+
33
+ .status {
34
+ height: 10px;
35
+ width: 10px;
36
+ border-radius: 50%;
37
+ display: inline-block;
38
+ }
@@ -21,7 +21,7 @@
21
21
  <div class="card">
22
22
  <%# Gate State Header %>
23
23
  <div class="card-header">
24
- <span class="octicon octicon-squirrel <%= @feature.color_class %> mr-2"></span>
24
+ <span class="status <%= @feature.color_class %> mr-2"></span>
25
25
  <%= @feature.gate_state_title %>
26
26
  </div>
27
27
 
@@ -69,8 +69,8 @@
69
69
  <%== csrf_input_tag %>
70
70
  <input type="hidden" name="operation" value="disable">
71
71
  <input type="hidden" name="value" value="<%= item %>">
72
- <button type="submit" value="Disable" class="btn btn-link btn-sm text-danger" data-toggle="tooltip" title="Disable <%= item %>" data-placement="left">
73
- <span class="octicon octicon-trashcan"></span>
72
+ <button type="submit" value="Disable" class="btn btn-outline-danger" data-toggle="tooltip" title="Disable <%= item %>" data-placement="left">
73
+ Remove
74
74
  </button>
75
75
  </form>
76
76
  </div>
@@ -40,7 +40,7 @@
40
40
  <% @features.each do |feature| %>
41
41
  <div class="feature row align-items-center mt-0 px-3 border-bottom">
42
42
  <div class="col-1 col-md-auto">
43
- <span class="octicon octicon-squirrel <%= feature.color_class %>" data-toggle="tooltip" title=<%= feature.state.to_s.capitalize %>></span>
43
+ <span class="status <%= feature.color_class %>" data-toggle="tooltip" title=<%= feature.state.to_s.capitalize %>></span>
44
44
  </div>
45
45
  <div class="col-10">
46
46
  <a href="<%= "#{script_name}/features/#{feature.key}" %>" class="d-block px-0 py-3 btn text-left text-dark">
@@ -6,8 +6,7 @@
6
6
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1">
8
8
 
9
- <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
10
- <link rel="stylesheet" href="<%= script_name %>/octicons/octicons.css">
9
+ <link rel="stylesheet" href="<%= bootstrap_css[:src] %>" integrity="<%= bootstrap_css[:hash] %>" crossorigin="anonymous">
11
10
  <link rel="stylesheet" href="<%= script_name %>/css/application.css">
12
11
  </head>
13
12
  <body class="py-4">
@@ -52,9 +51,9 @@
52
51
  <% end %>
53
52
  </div>
54
53
 
55
- <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
56
- <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
57
- <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
54
+ <script src="<%= jquery_js[:src] %>" integrity="<%= jquery_js[:hash] %>" crossorigin="anonymous"></script>
55
+ <script src="<%= popper_js[:src] %>" integrity="<%= popper_js[:hash] %>" crossorigin="anonymous"></script>
56
+ <script src="<%= bootstrap_js[:src] %>" integrity="<%= bootstrap_js[:hash] %>" crossorigin="anonymous"></script>
58
57
  <script src="<%= script_name %>/js/application.js"></script>
59
58
  </body>
60
59
  </html>
data/lib/flipper/ui.rb CHANGED
@@ -39,14 +39,14 @@ module Flipper
39
39
  def self.app(flipper = nil, options = {})
40
40
  env_key = options.fetch(:env_key, 'flipper')
41
41
  rack_protection_options = options.fetch(:rack_protection, use: :authenticity_token)
42
+
42
43
  app = ->(_) { [200, { 'Content-Type' => 'text/html' }, ['']] }
43
44
  builder = Rack::Builder.new
44
45
  yield builder if block_given?
45
46
  builder.use Rack::Protection, rack_protection_options
46
47
  builder.use Rack::MethodOverride
47
48
  builder.use Flipper::Middleware::SetupEnv, flipper, env_key: env_key
48
- builder.use Flipper::Middleware::Memoizer, env_key: env_key
49
- builder.use Flipper::UI::Middleware, env_key: env_key
49
+ builder.use Flipper::UI::Middleware, flipper: flipper, env_key: env_key
50
50
  builder.run app
51
51
  klass = self
52
52
  builder.define_singleton_method(:inspect) { klass.inspect } # pretty rake routes output
@@ -1,3 +1,3 @@
1
1
  module Flipper
2
- VERSION = '0.21.0.rc1'.freeze
2
+ VERSION = '0.22.1'.freeze
3
3
  end
@@ -61,6 +61,23 @@ RSpec.describe Flipper::UI::Actions::ActorsGate do
61
61
  expect(last_response.headers['Location']).to eq('/features/search')
62
62
  end
63
63
 
64
+ context "when feature name contains space" do
65
+ before do
66
+ post 'features/sp%20ace/actors',
67
+ { 'value' => value, 'operation' => 'enable', 'authenticity_token' => token },
68
+ 'rack.session' => session
69
+ end
70
+
71
+ it 'adds item to members' do
72
+ expect(flipper["sp ace"].actors_value).to include('User;6')
73
+ end
74
+
75
+ it "redirects back to feature" do
76
+ expect(last_response.status).to be(302)
77
+ expect(last_response.headers['Location']).to eq('/features/sp%20ace')
78
+ end
79
+ end
80
+
64
81
  context 'value contains whitespace' do
65
82
  let(:value) { ' User;6 ' }
66
83
 
@@ -75,7 +92,7 @@ RSpec.describe Flipper::UI::Actions::ActorsGate do
75
92
 
76
93
  it 'redirects back to feature' do
77
94
  expect(last_response.status).to be(302)
78
- expect(last_response.headers['Location']).to eq('/features/search/actors?error=%22%22+is+not+a+valid+actor+value.')
95
+ expect(last_response.headers['Location']).to eq('/features/search/actors?error=%22%22%20is%20not%20a%20valid%20actor%20value.')
79
96
  end
80
97
  end
81
98
 
@@ -84,7 +101,7 @@ RSpec.describe Flipper::UI::Actions::ActorsGate do
84
101
 
85
102
  it 'redirects back to feature' do
86
103
  expect(last_response.status).to be(302)
87
- expect(last_response.headers['Location']).to eq('/features/search/actors?error=%22%22+is+not+a+valid+actor+value.')
104
+ expect(last_response.headers['Location']).to eq('/features/search/actors?error=%22%22%20is%20not%20a%20valid%20actor%20value.')
88
105
  end
89
106
  end
90
107
  end