flipper-ui 0.2.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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