flipper-ui 0.2.0.beta1

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 (80) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +17 -0
  5. data/Guardfile +26 -0
  6. data/LICENSE +22 -0
  7. data/README.md +101 -0
  8. data/Rakefile +7 -0
  9. data/examples/basic.ru +44 -0
  10. data/examples/flipper.html +14 -0
  11. data/examples/flipper.png +0 -0
  12. data/flipper-ui.gemspec +21 -0
  13. data/lib/flipper-ui.rb +1 -0
  14. data/lib/flipper/ui.rb +23 -0
  15. data/lib/flipper/ui/action.rb +172 -0
  16. data/lib/flipper/ui/action_collection.rb +20 -0
  17. data/lib/flipper/ui/actions/features.rb +21 -0
  18. data/lib/flipper/ui/actions/file.rb +17 -0
  19. data/lib/flipper/ui/actions/gate.rb +143 -0
  20. data/lib/flipper/ui/actions/index.rb +17 -0
  21. data/lib/flipper/ui/assets/javascripts/application.coffee +305 -0
  22. data/lib/flipper/ui/assets/javascripts/spine/ajax.coffee +223 -0
  23. data/lib/flipper/ui/assets/javascripts/spine/list.coffee +43 -0
  24. data/lib/flipper/ui/assets/javascripts/spine/local.coffee +16 -0
  25. data/lib/flipper/ui/assets/javascripts/spine/manager.coffee +83 -0
  26. data/lib/flipper/ui/assets/javascripts/spine/relation.coffee +148 -0
  27. data/lib/flipper/ui/assets/javascripts/spine/route.coffee +146 -0
  28. data/lib/flipper/ui/assets/javascripts/spine/spine.coffee +542 -0
  29. data/lib/flipper/ui/assets/javascripts/spine/version +1 -0
  30. data/lib/flipper/ui/assets/stylesheets/application.scss +237 -0
  31. data/lib/flipper/ui/decorators/feature.rb +37 -0
  32. data/lib/flipper/ui/decorators/gate.rb +36 -0
  33. data/lib/flipper/ui/error.rb +10 -0
  34. data/lib/flipper/ui/eruby.rb +11 -0
  35. data/lib/flipper/ui/middleware.rb +66 -0
  36. data/lib/flipper/ui/public/css/application.css +183 -0
  37. data/lib/flipper/ui/public/css/images/animated-overlay.gif +0 -0
  38. data/lib/flipper/ui/public/css/images/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
  39. data/lib/flipper/ui/public/css/images/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
  40. data/lib/flipper/ui/public/css/images/ui-bg_flat_10_000000_40x100.png +0 -0
  41. data/lib/flipper/ui/public/css/images/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
  42. data/lib/flipper/ui/public/css/images/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
  43. data/lib/flipper/ui/public/css/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  44. data/lib/flipper/ui/public/css/images/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
  45. data/lib/flipper/ui/public/css/images/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
  46. data/lib/flipper/ui/public/css/images/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
  47. data/lib/flipper/ui/public/css/images/ui-icons_222222_256x240.png +0 -0
  48. data/lib/flipper/ui/public/css/images/ui-icons_228ef1_256x240.png +0 -0
  49. data/lib/flipper/ui/public/css/images/ui-icons_ef8c08_256x240.png +0 -0
  50. data/lib/flipper/ui/public/css/images/ui-icons_ffd27a_256x240.png +0 -0
  51. data/lib/flipper/ui/public/css/images/ui-icons_ffffff_256x240.png +0 -0
  52. data/lib/flipper/ui/public/css/jquery-ui-1.10.3.slider.min.css +5 -0
  53. data/lib/flipper/ui/public/images/logo.png +0 -0
  54. data/lib/flipper/ui/public/images/remove.png +0 -0
  55. data/lib/flipper/ui/public/js/application.js +544 -0
  56. data/lib/flipper/ui/public/js/handlebars.js +1992 -0
  57. data/lib/flipper/ui/public/js/jquery-ui-1.10.3.slider.min.js +6 -0
  58. data/lib/flipper/ui/public/js/jquery.js +9555 -0
  59. data/lib/flipper/ui/public/js/jquery.min.js +4 -0
  60. data/lib/flipper/ui/public/js/jquery.min.map +1 -0
  61. data/lib/flipper/ui/public/js/spine/ajax.js +320 -0
  62. data/lib/flipper/ui/public/js/spine/list.js +72 -0
  63. data/lib/flipper/ui/public/js/spine/local.js +29 -0
  64. data/lib/flipper/ui/public/js/spine/manager.js +157 -0
  65. data/lib/flipper/ui/public/js/spine/relation.js +260 -0
  66. data/lib/flipper/ui/public/js/spine/route.js +223 -0
  67. data/lib/flipper/ui/public/js/spine/spine.js +927 -0
  68. data/lib/flipper/ui/util.rb +12 -0
  69. data/lib/flipper/ui/version.rb +5 -0
  70. data/lib/flipper/ui/views/index.erb +9 -0
  71. data/lib/flipper/ui/views/layout.erb +161 -0
  72. data/script/bootstrap +21 -0
  73. data/script/server +19 -0
  74. data/script/test +30 -0
  75. data/spec/flipper/ui/decorators/feature_spec.rb +59 -0
  76. data/spec/flipper/ui/decorators/gate_spec.rb +47 -0
  77. data/spec/flipper/ui/util_spec.rb +18 -0
  78. data/spec/flipper/ui_spec.rb +470 -0
  79. data/spec/helper.rb +35 -0
  80. metadata +168 -0
@@ -0,0 +1,12 @@
1
+ module Flipper
2
+ module UI
3
+ module Util
4
+ # Private: 0x3000: fullwidth whitespace
5
+ NON_WHITESPACE_REGEXP = %r![^\s#{[0x3000].pack("U")}]!
6
+
7
+ def self.blank?(str)
8
+ str.to_s !~ NON_WHITESPACE_REGEXP
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ module Flipper
2
+ module UI
3
+ VERSION = "0.2.0.beta1"
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ <div id="features">
2
+ <p>loading indicator...</p>
3
+ </div>
4
+
5
+ <div id="no_features">
6
+ <h1>Ready to start flipping?</h1>
7
+ <p>You haven't created any features yet</p>
8
+ <p>Start by reading the <a href='https://github.com/jnunemaker/flipper#usage'>flipper readme</p>
9
+ </div>
@@ -0,0 +1,161 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>Flipper</title>
5
+ <link rel="stylesheet" type="text/css" href="<%= script_name %>/css/application.css">
6
+ <link rel="stylesheet" type="text/css" href="<%= script_name %>/css/jquery-ui-1.10.3.slider.min.css">
7
+
8
+ <script>
9
+ var Flipper = {};
10
+ Flipper.Config = {
11
+ url: "<%= script_name %>"
12
+ }
13
+ </script>
14
+ <script src="<%= script_name %>/js/jquery.min.js"></script>
15
+ <script src="<%= script_name %>/js/jquery-ui-1.10.3.slider.min.js"></script>
16
+
17
+ <script src="<%= script_name %>/js/handlebars.js"></script>
18
+ <script src="<%= script_name %>/js/spine/spine.js"></script>
19
+ <script src="<%= script_name %>/js/spine/ajax.js"></script>
20
+ <script src="<%= script_name %>/js/spine/route.js"></script>
21
+ <script src="<%= script_name %>/js/spine/manager.js"></script>
22
+ <script src="<%= script_name %>/js/application.js"></script>
23
+ </head>
24
+ <body>
25
+ <div id="app">
26
+ <div id="header">
27
+ <h1>
28
+ <a href="<%= script_name %>/">
29
+ <img src="<%= script_name %>/images/logo.png" alt="Flipper" />
30
+ </a>
31
+ </h1>
32
+ </div>
33
+
34
+ <div id="content">
35
+ <%== yield %>
36
+ </div>
37
+ </div>
38
+
39
+ <script id="feature-template" type="text/x-handlebars-template">
40
+ <div class="feature" id="feature-{{id}}">
41
+ <div class="show">
42
+ <h3>
43
+ <span class="state {{state}}"></span>
44
+ {{name}}
45
+ <span class="description">{{description}}</span>
46
+ </h3>
47
+
48
+ <div class="actions">
49
+ <a href="#" class="show-settings">Change Settings</a>
50
+ </div>
51
+ </div>
52
+
53
+ <div class="edit">
54
+ <h3>
55
+ {{name}}
56
+ <span class="description">Settings</span>
57
+ </h3>
58
+
59
+ <div class="actions">
60
+ <a href="#" class="hide-settings">Cancel</a>
61
+ </div>
62
+
63
+ <div class="tabs">
64
+ <ul>
65
+ <li><a href="#" data-tab="boolean">All On/All Off</a></li>
66
+ <li><a href="#" data-tab="group">Groups</a></li>
67
+ <li><a href="#" data-tab="actor">Actors</a></li>
68
+ <li><a href="#" data-tab="percentage_of_actors">
69
+ Percentage Of Actors</a></li>
70
+ <li><a href="#" data-tab="percentage_of_random">
71
+ Percentage Of Random</a></li>
72
+ </ul>
73
+ </div>
74
+
75
+ <div class="gates"></div>
76
+ </div>
77
+ </div>
78
+ </script>
79
+
80
+ <script id="gate-boolean-template" type="text/x-handlebars-template">
81
+ <form action="/features/{{feature_id}}/boolean" method="POST">
82
+ <input type="radio" name="value" value="true" {{#value}}checked="checked"{{/value}} /> On
83
+ <input type="radio" name="value" value="false" {{^value}}checked="checked"{{/value}} /> Off
84
+ <button>Save Settings</button>
85
+ </form>
86
+ </script>
87
+
88
+ <script id="gate-member-template" type="text/x-handlebars-template">
89
+ <li class="member" data-value="{{this}}">
90
+ <a href="#" class="disable">
91
+ <img src="<%= script_name %>/images/remove.png">
92
+ </a>
93
+ {{this}}
94
+ </li>
95
+ </script>
96
+
97
+ <script id="gate-group-template" type="text/x-handlebars-template">
98
+ <form action="/features/{{feature_id}}/group" method="POST">
99
+ <input type="hidden" name="operation" value="enable" />
100
+ <label>
101
+ Group Name
102
+ <select name="value">
103
+ <option value="">Select the group...</option>
104
+ <% Flipper.group_names.each do |name| %>
105
+ <option value="<%= name %>"><%= name %></option>
106
+ <% end %>
107
+ </select>
108
+ </label>
109
+ <button>Enable Group</button>
110
+ </form>
111
+ <ul class="members">
112
+ {{#each value}}
113
+ <li class="member" data-value="{{this}}">
114
+ <a href="#" class="disable">
115
+ <img src="<%= script_name %>/images/remove.png">
116
+ </a>
117
+ {{this}}
118
+ </li>
119
+ {{/each}}
120
+ </ul>
121
+ </script>
122
+
123
+ <script id="gate-actor-template" type="text/x-handlebars-template">
124
+ <form action="/features/{{feature_id}}/actor" method="POST">
125
+ <input type="hidden" name="operation" value="enable" />
126
+ <label>
127
+ Flipper Id
128
+ <input type="text" name="value" value="" />
129
+ </label>
130
+ <button>Enable Actor</button>
131
+ </form>
132
+ <ul class="members">
133
+ {{#each value}}
134
+ <li class="member" data-value="{{this}}">
135
+ <a href="#" class="disable">
136
+ <img src="<%= script_name %>/images/remove.png">
137
+ </a>
138
+ {{this}}
139
+ </li>
140
+ {{/each}}
141
+ </ul>
142
+ </script>
143
+
144
+ <script id="gate-percentage-of-actors-template" type="text/x-handlebars-template">
145
+ <form action="/features/{{feature_id}}/percentage_of_actors" method="POST">
146
+ <div class="slider-range"></div>
147
+ <input type="text" name="value" value="{{value}}" /> %
148
+ <button>Save Settings</button>
149
+ </form>
150
+ </script>
151
+
152
+ <script id="gate-percentage-of-random-template" type="text/x-handlebars-template">
153
+ <form action="/features/{{feature_id}}/percentage_of_random" method="POST">
154
+ <div class="slider-range"></div>
155
+ <input type="text" name="value" value="{{value}}" /> %
156
+ <button>Save Settings</button>
157
+ </form>
158
+ </script>
159
+
160
+ </body>
161
+ </html>
data/script/bootstrap ADDED
@@ -0,0 +1,21 @@
1
+ #!/bin/sh
2
+ #/ Usage: bootstrap [bundle options]
3
+ #/
4
+ #/ Bundle install the dependencies.
5
+ #/
6
+ #/ Examples:
7
+ #/
8
+ #/ bootstrap
9
+ #/ bootstrap --local
10
+ #/
11
+
12
+ set -e
13
+ cd $(dirname "$0")/..
14
+
15
+ [ "$1" = "--help" -o "$1" = "-h" -o "$1" = "help" ] && {
16
+ grep '^#/' <"$0"| cut -c4-
17
+ exit 0
18
+ }
19
+
20
+ rm -rf .bundle/{binstubs,config}
21
+ bundle install --binstubs .bundle/binstubs --path .bundle --quiet "$@"
data/script/server ADDED
@@ -0,0 +1,19 @@
1
+ #!/bin/sh
2
+ #/ Usage: server
3
+ #/
4
+ #/ Starts a server for perusing the UI locally.
5
+ #/
6
+ #/ Examples:
7
+ #/
8
+ #/ server
9
+ #/
10
+
11
+ set -e
12
+ cd $(dirname "$0")/..
13
+
14
+ [ "$1" = "--help" -o "$1" = "-h" -o "$1" = "help" ] && {
15
+ grep '^#/' <"$0"| cut -c4-
16
+ exit 0
17
+ }
18
+
19
+ bundle exec rackup examples/basic.ru -p 9999
data/script/test ADDED
@@ -0,0 +1,30 @@
1
+ #!/bin/sh
2
+ #/ Usage: test [individual test file]
3
+ #/
4
+ #/ Bootstrap and run all tests or an individual test.
5
+ #/
6
+ #/ Examples:
7
+ #/
8
+ #/ # run all tests
9
+ #/ test
10
+ #/
11
+ #/ # run individual test
12
+ #/ test spec/qu_spec.rb
13
+ #/
14
+
15
+ set -e
16
+ cd $(dirname "$0")/..
17
+
18
+ [ "$1" = "--help" -o "$1" = "-h" -o "$1" = "help" ] && {
19
+ grep '^#/' <"$0"| cut -c4-
20
+ exit 0
21
+ }
22
+
23
+ specs="spec/"
24
+
25
+ if [ $# -gt 0 ]
26
+ then
27
+ specs=$@
28
+ fi
29
+
30
+ script/bootstrap && bundle exec rspec $specs
@@ -0,0 +1,59 @@
1
+ require 'helper'
2
+ require 'flipper/adapters/memory'
3
+
4
+ describe Flipper::UI::Decorators::Feature do
5
+ let(:source) { {} }
6
+ let(:adapter) { Flipper::Adapters::Memory.new(source) }
7
+ let(:flipper) { Flipper.new(adapter) }
8
+ let(:feature) { flipper[:some_awesome_feature] }
9
+
10
+ subject {
11
+ described_class.new(feature)
12
+ }
13
+
14
+ describe "#initialize" do
15
+ it "sets the feature" do
16
+ subject.feature.should be(feature)
17
+ end
18
+ end
19
+
20
+ describe "#pretty_name" do
21
+ it "capitalizes each word separated by underscores" do
22
+ subject.pretty_name.should eq('Some Awesome Feature')
23
+ end
24
+ end
25
+
26
+ describe "#as_json" do
27
+ before do
28
+ @result = subject.as_json
29
+ end
30
+
31
+ it "returns Hash" do
32
+ @result.should be_instance_of(Hash)
33
+ end
34
+
35
+ it "includes id" do
36
+ @result['id'].should eq('some_awesome_feature')
37
+ end
38
+
39
+ it "includes pretty name" do
40
+ @result['name'].should eq('Some Awesome Feature')
41
+ end
42
+
43
+ it "includes state" do
44
+ @result['state'].should eq('off')
45
+ end
46
+
47
+ it "includes description" do
48
+ @result['description'].should eq('Disabled')
49
+ end
50
+
51
+ it "includes gates" do
52
+ gates = subject.gates.map { |gate|
53
+ value = subject.gate_values[gate.key]
54
+ Flipper::UI::Decorators::Gate.new(gate, value).as_json
55
+ }
56
+ @result['gates'].should eq(gates)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,47 @@
1
+ require 'helper'
2
+ require 'flipper/adapters/memory'
3
+ require 'flipper/ui/decorators/gate'
4
+
5
+ describe Flipper::UI::Decorators::Gate do
6
+ let(:source) { {} }
7
+ let(:adapter) { Flipper::Adapters::Memory.new(source) }
8
+ let(:flipper) { Flipper.new(adapter) }
9
+ let(:feature) { flipper[:some_awesome_feature] }
10
+ let(:gate) { feature.gate(:boolean) }
11
+
12
+ subject {
13
+ described_class.new(gate, false)
14
+ }
15
+
16
+ describe "#initialize" do
17
+ it "sets gate" do
18
+ subject.gate.should be(gate)
19
+ end
20
+
21
+ it "sets value" do
22
+ subject.value.should eq(false)
23
+ end
24
+ end
25
+
26
+ describe "#as_json" do
27
+ before do
28
+ @result = subject.as_json
29
+ end
30
+
31
+ it "returns Hash" do
32
+ @result.should be_instance_of(Hash)
33
+ end
34
+
35
+ it "includes key" do
36
+ @result['key'].should eq('boolean')
37
+ end
38
+
39
+ it "includes pretty name" do
40
+ @result['name'].should eq('boolean')
41
+ end
42
+
43
+ it "includes value" do
44
+ @result['value'].should be(false)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,18 @@
1
+ require 'helper'
2
+ require 'flipper/ui/util'
3
+
4
+ describe Flipper::UI::Util do
5
+ describe "#blank?" do
6
+ context "with a string" do
7
+ it "returns true if blank" do
8
+ described_class.blank?(nil).should be_true
9
+ described_class.blank?('').should be_true
10
+ described_class.blank?(' ').should be_true
11
+ end
12
+
13
+ it "returns false if not blank" do
14
+ described_class.blank?('nope').should be_false
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,470 @@
1
+ require 'helper'
2
+ require 'rack/test'
3
+ require 'flipper'
4
+ require 'flipper/adapters/memory'
5
+ require 'open-uri'
6
+
7
+ describe Flipper::UI do
8
+ include Rack::Test::Methods
9
+
10
+ let(:source) { {} }
11
+ let(:adapter) { Flipper::Adapters::Memory.new(source) }
12
+
13
+ let(:flipper) {
14
+ Flipper.new(adapter, :instrumenter => ActiveSupport::Notifications)
15
+ }
16
+
17
+ let(:app) { described_class.app(flipper) }
18
+
19
+ describe "Initializing middleware lazily with a block" do
20
+ let(:app) { described_class.app(lambda { flipper }) }
21
+
22
+ it "works" do
23
+ get '/features'
24
+ last_response.status.should be(200)
25
+ end
26
+ end
27
+
28
+ describe "GET /" do
29
+ before do
30
+ flipper[:stats].enable
31
+ flipper[:search].enable
32
+ get '/'
33
+ end
34
+
35
+ it "responds with 200" do
36
+ last_response.status.should be(200)
37
+ end
38
+
39
+ it "renders view" do
40
+ last_response.body.should match(/Flipper/)
41
+ end
42
+ end
43
+
44
+ describe "GET /features" do
45
+ before do
46
+ flipper[:new_stats].enable
47
+ flipper[:search].disable
48
+ get '/features'
49
+ end
50
+
51
+ it "responds with 200" do
52
+ last_response.status.should be(200)
53
+ end
54
+
55
+ it "responds with content type of json" do
56
+ last_response.content_type.should eq('application/json')
57
+ end
58
+
59
+ it "renders view" do
60
+ features = json_response
61
+ features.should be_instance_of(Array)
62
+
63
+ feature = features[0]
64
+ feature['id'].should eq('new_stats')
65
+ feature['name'].should eq('New Stats')
66
+ feature['state'].should eq('on')
67
+ feature['description'].should eq('Enabled')
68
+ feature['gates'].first.should eq({
69
+ 'key' => 'boolean',
70
+ 'name' => 'boolean',
71
+ 'value' => true,
72
+ })
73
+
74
+ feature = features[1]
75
+ feature['id'].should eq('search')
76
+ feature['name'].should eq('Search')
77
+ feature['state'].should eq('off')
78
+ feature['description'].should eq('Disabled')
79
+ feature['gates'].first.should eq({
80
+ 'key' => 'boolean',
81
+ 'name' => 'boolean',
82
+ 'value' => false,
83
+ })
84
+ end
85
+ end
86
+
87
+ describe "POST /features/:id/non_existent_gate_name" do
88
+ before do
89
+ feature = flipper[:some_thing]
90
+ params = {
91
+ 'value' => 'something',
92
+ }
93
+ post "/features/#{feature.name}/non_existent_gate_name", params
94
+ end
95
+
96
+ it "responds with 404" do
97
+ last_response.status.should be(404)
98
+ end
99
+
100
+ it "includes status and message" do
101
+ result = json_response
102
+ result['status'].should eq('error')
103
+ result['message'].should eq('I have no clue how to update the gate named "non_existent_gate_name".')
104
+ end
105
+ end
106
+
107
+ describe "POST /features/:id/boolean" do
108
+ before do
109
+ feature = flipper[:some_thing]
110
+ feature.enable
111
+ params = {
112
+ 'value' => 'false',
113
+ }
114
+ post "/features/#{feature.name}/boolean", params
115
+ end
116
+
117
+ it "responds with 200" do
118
+ last_response.status.should be(200)
119
+ end
120
+
121
+ it "responds with json" do
122
+ result = json_response
123
+ result.should be_instance_of(Hash)
124
+ result['name'].should eq('boolean')
125
+ result['key'].should eq('boolean')
126
+ result['value'].should eq(false)
127
+ end
128
+
129
+ it "updates gate state" do
130
+ flipper[:some_thing].state.should be(:off)
131
+ end
132
+ end
133
+
134
+ describe "POST /features/:id/boolean for values that are URI encoded" do
135
+ before do
136
+ feature = flipper["feature:v1"]
137
+ feature.enable
138
+ params = {
139
+ 'value' => 'false',
140
+ }
141
+ feature_encoded_name = URI.encode_www_form_component(feature.name)
142
+ post "/features/#{feature_encoded_name}/boolean", params
143
+ end
144
+
145
+ it "responds with 200" do
146
+ last_response.status.should be(200)
147
+ end
148
+
149
+ it "responds with json" do
150
+ result = json_response
151
+ result.should be_instance_of(Hash)
152
+ result['name'].should eq('boolean')
153
+ result['key'].should eq('boolean')
154
+ result['value'].should eq(false)
155
+ end
156
+
157
+ it "updates gate state" do
158
+ flipper["feature:v1"].state.should be(:off)
159
+ end
160
+ end
161
+
162
+ describe "POST /features/:id/percentage_of_actors" do
163
+ context "valid value" do
164
+ before do
165
+ feature = flipper[:some_thing]
166
+ params = {
167
+ 'value' => '5',
168
+ }
169
+ post "/features/#{feature.name}/percentage_of_actors", params
170
+ end
171
+
172
+ it "responds with 200" do
173
+ last_response.status.should be(200)
174
+ end
175
+
176
+ it "responds with json" do
177
+ result = json_response
178
+ result.should be_instance_of(Hash)
179
+ result['name'].should eq('percentage_of_actors')
180
+ result['key'].should eq('percentage_of_actors')
181
+ result['value'].should eq(5)
182
+ end
183
+
184
+ it "updates gate state" do
185
+ gate_value(:some_thing, :percentage_of_actors).to_i.should be(5)
186
+ end
187
+ end
188
+
189
+ context "invalid value" do
190
+ before do
191
+ feature = flipper[:some_thing]
192
+ params = {
193
+ 'value' => '555',
194
+ }
195
+ post "/features/#{feature.name}/percentage_of_actors", params
196
+ end
197
+
198
+ it "responds with 422" do
199
+ last_response.status.should be(422)
200
+ end
201
+
202
+ it "includes status and message in response" do
203
+ result = json_response
204
+ result['status'].should eq('error')
205
+ result['message'].should eq('value must be a positive number less than or equal to 100, but was 555')
206
+ end
207
+ end
208
+ end
209
+
210
+ describe "POST /features/:id/percentage_of_random" do
211
+ context "valid value" do
212
+ before do
213
+ feature = flipper[:some_thing]
214
+ params = {
215
+ 'value' => '5',
216
+ }
217
+ post "/features/#{feature.name}/percentage_of_random", params
218
+ end
219
+
220
+ it "responds with 200" do
221
+ last_response.status.should be(200)
222
+ end
223
+
224
+ it "responds with json" do
225
+ result = json_response
226
+ result.should be_instance_of(Hash)
227
+ result['name'].should eq('percentage_of_random')
228
+ result['key'].should eq('percentage_of_random')
229
+ result['value'].should eq(5)
230
+ end
231
+
232
+ it "updates gate state" do
233
+ gate_value(:some_thing, :percentage_of_random).to_i.should be(5)
234
+ end
235
+ end
236
+
237
+ context "invalid value" do
238
+ before do
239
+ feature = flipper[:some_thing]
240
+ params = {
241
+ 'value' => '555',
242
+ }
243
+ post "/features/#{feature.name}/percentage_of_random", params
244
+ end
245
+
246
+ it "responds with 422" do
247
+ last_response.status.should be(422)
248
+ end
249
+
250
+ it "includes status and message in response" do
251
+ result = json_response
252
+ result['status'].should eq('error')
253
+ result['message'].should eq('value must be a positive number less than or equal to 100, but was 555')
254
+ end
255
+ end
256
+ end
257
+
258
+ describe "POST /features/:id/actor" do
259
+ context "enable" do
260
+ before do
261
+ feature = flipper[:some_thing]
262
+ params = {
263
+ 'operation' => 'enable',
264
+ 'value' => '11',
265
+ }
266
+ post "/features/#{feature.name}/actor", params
267
+ end
268
+
269
+ it "responds with 200" do
270
+ last_response.status.should be(200)
271
+ end
272
+
273
+ it "responds with json" do
274
+ result = json_response
275
+ result.should be_instance_of(Hash)
276
+ result['name'].should eq('actor')
277
+ result['key'].should eq('actors')
278
+ result['value'].should eq(['11'])
279
+ end
280
+
281
+ it "updates gate state" do
282
+ gate_value(:some_thing, :actors).should include('11')
283
+ end
284
+ end
285
+
286
+ context "disable" do
287
+ before do
288
+ feature = flipper[:some_thing]
289
+ feature.enable Struct.new(:flipper_id).new('11')
290
+ params = {
291
+ 'operation' => 'disable',
292
+ 'value' => '11',
293
+ }
294
+ post "/features/#{feature.name}/actor", params
295
+ end
296
+
297
+ it "responds with 200" do
298
+ last_response.status.should be(200)
299
+ end
300
+
301
+ it "responds with json" do
302
+ result = json_response
303
+ result.should be_instance_of(Hash)
304
+ result['name'].should eq('actor')
305
+ result['key'].should eq('actors')
306
+ result['value'].should eq([])
307
+ end
308
+
309
+ it "updates gate state" do
310
+ gate_value(:some_thing, :actors).should_not include('11')
311
+ end
312
+ end
313
+
314
+ context "invalid value" do
315
+ before do
316
+ feature = flipper[:some_thing]
317
+ params = {
318
+ 'operation' => 'enable',
319
+ 'value' => '',
320
+ }
321
+ post "/features/#{feature.name}/actor", params
322
+ end
323
+
324
+ it "responds with 422" do
325
+ last_response.status.should be(422)
326
+ end
327
+
328
+ it "updates gate state" do
329
+ result = json_response
330
+ result['status'].should eq('error')
331
+ result['message'].should eq('"" is not a valid actor value.')
332
+ end
333
+ end
334
+ end
335
+
336
+ describe "POST /features/:id/group" do
337
+ before do
338
+ Flipper.register(:admins) { |user| user.admin? }
339
+ end
340
+
341
+ after do
342
+ Flipper.unregister_groups
343
+ end
344
+
345
+ context "enable" do
346
+ before do
347
+ feature = flipper[:some_thing]
348
+ params = {
349
+ 'operation' => 'enable',
350
+ 'value' => 'admins',
351
+ }
352
+ post "/features/#{feature.name}/group", params
353
+ end
354
+
355
+ it "responds with 200" do
356
+ last_response.status.should be(200)
357
+ end
358
+
359
+ it "responds with json" do
360
+ result = json_response
361
+ result.should be_instance_of(Hash)
362
+ result['name'].should eq('group')
363
+ result['key'].should eq('groups')
364
+ result['value'].should eq(['admins'])
365
+ end
366
+
367
+ it "updates gate state" do
368
+ gate_value(:some_thing, :groups).should include('admins')
369
+ end
370
+ end
371
+
372
+ context "disable" do
373
+ before do
374
+ feature = flipper[:some_thing]
375
+ feature.enable flipper.group(:admins)
376
+ params = {
377
+ 'operation' => 'disable',
378
+ 'value' => 'admins',
379
+ }
380
+ post "/features/#{feature.name}/group", params
381
+ end
382
+
383
+ it "responds with 200" do
384
+ last_response.status.should be(200)
385
+ end
386
+
387
+ it "responds with json" do
388
+ result = json_response
389
+ result.should be_instance_of(Hash)
390
+ result['name'].should eq('group')
391
+ result['key'].should eq('groups')
392
+ result['value'].should eq([])
393
+ end
394
+
395
+ it "updates gate state" do
396
+ gate_value(:some_thing, :groups).should_not include('admins')
397
+ end
398
+ end
399
+
400
+ context "when group is not found" do
401
+ before do
402
+ feature = flipper[:some_thing]
403
+ params = {
404
+ 'operation' => 'enable',
405
+ 'value' => 'not_here',
406
+ }
407
+ post "/features/#{feature.name}/group", params
408
+ end
409
+
410
+ it "responds with 404" do
411
+ last_response.status.should be(404)
412
+ end
413
+ end
414
+
415
+ context "when group is empty" do
416
+ before do
417
+ feature = flipper[:some_thing]
418
+ params = {
419
+ 'operation' => 'enable',
420
+ 'value' => '',
421
+ }
422
+ post "/features/#{feature.name}/group", params
423
+ end
424
+
425
+ it "responds with 422" do
426
+ last_response.status.should be(422)
427
+ end
428
+
429
+ it "has message in body" do
430
+ hash = JSON.load(last_response.body)
431
+ hash["status"].should eq("error")
432
+ hash["message"].should eq("Group name is required.")
433
+ end
434
+ end
435
+ end
436
+
437
+ describe "GET /images/logo.png" do
438
+ before do
439
+ get '/images/logo.png'
440
+ end
441
+
442
+ it "responds with 200" do
443
+ last_response.status.should be(200)
444
+ end
445
+ end
446
+
447
+ describe "GET /css/application.css" do
448
+ before do
449
+ get '/css/application.css'
450
+ end
451
+
452
+ it "responds with 200" do
453
+ last_response.status.should be(200)
454
+ end
455
+ end
456
+
457
+ context "Request method unsupported by action" do
458
+ it "raises error" do
459
+ expect {
460
+ post '/images/logo.png'
461
+ }.to raise_error(Flipper::UI::RequestMethodNotSupported)
462
+ end
463
+ end
464
+
465
+ # Gets the adapter value for a given feature name and gate key.
466
+ def gate_value(feature_name, gate_key)
467
+ values = flipper.adapter.get(flipper[feature_name])
468
+ values[gate_key]
469
+ end
470
+ end