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.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rspec +1 -0
- data/Gemfile +17 -0
- data/Guardfile +26 -0
- data/LICENSE +22 -0
- data/README.md +101 -0
- data/Rakefile +7 -0
- data/examples/basic.ru +44 -0
- data/examples/flipper.html +14 -0
- data/examples/flipper.png +0 -0
- data/flipper-ui.gemspec +21 -0
- data/lib/flipper-ui.rb +1 -0
- data/lib/flipper/ui.rb +23 -0
- data/lib/flipper/ui/action.rb +172 -0
- data/lib/flipper/ui/action_collection.rb +20 -0
- data/lib/flipper/ui/actions/features.rb +21 -0
- data/lib/flipper/ui/actions/file.rb +17 -0
- data/lib/flipper/ui/actions/gate.rb +143 -0
- data/lib/flipper/ui/actions/index.rb +17 -0
- data/lib/flipper/ui/assets/javascripts/application.coffee +305 -0
- data/lib/flipper/ui/assets/javascripts/spine/ajax.coffee +223 -0
- data/lib/flipper/ui/assets/javascripts/spine/list.coffee +43 -0
- data/lib/flipper/ui/assets/javascripts/spine/local.coffee +16 -0
- data/lib/flipper/ui/assets/javascripts/spine/manager.coffee +83 -0
- data/lib/flipper/ui/assets/javascripts/spine/relation.coffee +148 -0
- data/lib/flipper/ui/assets/javascripts/spine/route.coffee +146 -0
- data/lib/flipper/ui/assets/javascripts/spine/spine.coffee +542 -0
- data/lib/flipper/ui/assets/javascripts/spine/version +1 -0
- data/lib/flipper/ui/assets/stylesheets/application.scss +237 -0
- data/lib/flipper/ui/decorators/feature.rb +37 -0
- data/lib/flipper/ui/decorators/gate.rb +36 -0
- data/lib/flipper/ui/error.rb +10 -0
- data/lib/flipper/ui/eruby.rb +11 -0
- data/lib/flipper/ui/middleware.rb +66 -0
- data/lib/flipper/ui/public/css/application.css +183 -0
- data/lib/flipper/ui/public/css/images/animated-overlay.gif +0 -0
- data/lib/flipper/ui/public/css/images/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
- data/lib/flipper/ui/public/css/images/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
- data/lib/flipper/ui/public/css/images/ui-bg_flat_10_000000_40x100.png +0 -0
- data/lib/flipper/ui/public/css/images/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
- data/lib/flipper/ui/public/css/images/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
- data/lib/flipper/ui/public/css/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/lib/flipper/ui/public/css/images/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
- data/lib/flipper/ui/public/css/images/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
- data/lib/flipper/ui/public/css/images/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
- data/lib/flipper/ui/public/css/images/ui-icons_222222_256x240.png +0 -0
- data/lib/flipper/ui/public/css/images/ui-icons_228ef1_256x240.png +0 -0
- data/lib/flipper/ui/public/css/images/ui-icons_ef8c08_256x240.png +0 -0
- data/lib/flipper/ui/public/css/images/ui-icons_ffd27a_256x240.png +0 -0
- data/lib/flipper/ui/public/css/images/ui-icons_ffffff_256x240.png +0 -0
- data/lib/flipper/ui/public/css/jquery-ui-1.10.3.slider.min.css +5 -0
- data/lib/flipper/ui/public/images/logo.png +0 -0
- data/lib/flipper/ui/public/images/remove.png +0 -0
- data/lib/flipper/ui/public/js/application.js +544 -0
- data/lib/flipper/ui/public/js/handlebars.js +1992 -0
- data/lib/flipper/ui/public/js/jquery-ui-1.10.3.slider.min.js +6 -0
- data/lib/flipper/ui/public/js/jquery.js +9555 -0
- data/lib/flipper/ui/public/js/jquery.min.js +4 -0
- data/lib/flipper/ui/public/js/jquery.min.map +1 -0
- data/lib/flipper/ui/public/js/spine/ajax.js +320 -0
- data/lib/flipper/ui/public/js/spine/list.js +72 -0
- data/lib/flipper/ui/public/js/spine/local.js +29 -0
- data/lib/flipper/ui/public/js/spine/manager.js +157 -0
- data/lib/flipper/ui/public/js/spine/relation.js +260 -0
- data/lib/flipper/ui/public/js/spine/route.js +223 -0
- data/lib/flipper/ui/public/js/spine/spine.js +927 -0
- data/lib/flipper/ui/util.rb +12 -0
- data/lib/flipper/ui/version.rb +5 -0
- data/lib/flipper/ui/views/index.erb +9 -0
- data/lib/flipper/ui/views/layout.erb +161 -0
- data/script/bootstrap +21 -0
- data/script/server +19 -0
- data/script/test +30 -0
- data/spec/flipper/ui/decorators/feature_spec.rb +59 -0
- data/spec/flipper/ui/decorators/gate_spec.rb +47 -0
- data/spec/flipper/ui/util_spec.rb +18 -0
- data/spec/flipper/ui_spec.rb +470 -0
- data/spec/helper.rb +35 -0
- metadata +168 -0
|
@@ -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
|