|
4 | 4 | let(:admin) { double 'Fake Fliper Thing', flipper_id: 10 } |
5 | 5 |
|
6 | 6 | describe 'get' do |
| 7 | + malformed_queries = { |
| 8 | + 'array-shaped exclude_gate_names' => 'exclude_gate_names[]=true', |
| 9 | + 'hash-shaped exclude_gates' => 'exclude_gates[a]=true', |
| 10 | + 'hash-shaped keys' => 'keys[a]=my_feature', |
| 11 | + 'nested array-shaped keys' => 'keys[][]=my_feature', |
| 12 | + 'a bare percent escape' => 'keys=%', |
| 13 | + 'a truncated percent escape' => 'keys=%2', |
| 14 | + 'an invalid percent escape in a value' => 'keys=%GG', |
| 15 | + 'an invalid percent escape in a key' => '%GG=value', |
| 16 | + 'invalid UTF-8 in a key' => '%FF=value&keys=my_feature', |
| 17 | + 'invalid UTF-8' => 'keys=%FF', |
| 18 | + 'conflicting parameter shapes' => 'a=1&a[]=2', |
| 19 | + 'excessive nesting' => "a#{'[a]' * 150}=1", |
| 20 | + 'excessive parameter count' => (1..5000).map { |index| "ignored#{index}=1" }.join('&'), |
| 21 | + } |
| 22 | + |
| 23 | + malformed_queries.each do |description, query| |
| 24 | + it "fails open for #{description}" do |
| 25 | + flipper[:my_feature].enable |
| 26 | + expected_response = raw_api_get('/features', '') |
| 27 | + |
| 28 | + response = raw_api_get('/features', query) |
| 29 | + |
| 30 | + expect(response.first).to eq(200) |
| 31 | + expect(response).to eq(expected_response) |
| 32 | + end |
| 33 | + end |
| 34 | + |
| 35 | + it 'fully fails open when malformed syntax is combined with a valid option' do |
| 36 | + flipper[:my_feature].enable |
| 37 | + expected_response = raw_api_get('/features', '') |
| 38 | + |
| 39 | + response = raw_api_get('/features', 'a=1&a[]=2&exclude_gate_names=true') |
| 40 | + |
| 41 | + expect(response.first).to eq(200) |
| 42 | + expect(response).to eq(expected_response) |
| 43 | + end |
| 44 | + |
| 45 | + separator_queries = [ |
| 46 | + '&exclude_gate_names=true', |
| 47 | + '&&exclude_gate_names=true&&', |
| 48 | + ';exclude_gate_names=true', |
| 49 | + ';;exclude_gate_names=true;;', |
| 50 | + 'exclude_gate_names=true&&;;', |
| 51 | + ] |
| 52 | + |
| 53 | + separator_queries.each do |query| |
| 54 | + it "preserves optional parameter semantics with empty query segments in #{query.inspect}" do |
| 55 | + flipper[:my_feature].enable |
| 56 | + expected_response = raw_api_get('/features', 'exclude_gate_names=true') |
| 57 | + |
| 58 | + response = raw_api_get('/features', query) |
| 59 | + |
| 60 | + expect(response.first).to eq(200) |
| 61 | + expect(response).to eq(expected_response) |
| 62 | + end |
| 63 | + end |
| 64 | + |
| 65 | + it 'fails open when decoding a raw query component raises ArgumentError' do |
| 66 | + request = instance_double(Rack::Request, query_string: 'keys=%') |
| 67 | + action = described_class.new(flipper, request) |
| 68 | + allow(action).to receive(:safe_params).and_return('keys' => 'my_feature') |
| 69 | + allow(Rack::Utils).to receive(:unescape).with('keys').and_return('keys') |
| 70 | + allow(Rack::Utils).to receive(:unescape).with('%').and_raise(ArgumentError) |
| 71 | + |
| 72 | + expect(action.send(:requested_feature_names)).to be_nil |
| 73 | + end |
| 74 | + |
| 75 | + it 'uses parsed keys when no raw keys parameter is present' do |
| 76 | + request = instance_double(Rack::Request, query_string: 'other=value') |
| 77 | + action = described_class.new(flipper, request) |
| 78 | + allow(action).to receive(:safe_params).and_return('keys' => 'my_feature,other_feature') |
| 79 | + |
| 80 | + expect(action.send(:requested_feature_names)).to eq(%w[my_feature other_feature]) |
| 81 | + end |
| 82 | + |
| 83 | + { |
| 84 | + 'keys=my_feature&keys=other_feature' => %w[my_feature other_feature], |
| 85 | + 'keys=&keys=my_feature' => %w[my_feature], |
| 86 | + '=&keys=my_feature&=value' => %w[my_feature], |
| 87 | + 'keys=' => [], |
| 88 | + }.each do |query, expected_keys| |
| 89 | + it "preserves duplicate and blank parameter semantics for #{query.inspect}" do |
| 90 | + flipper[:my_feature].enable |
| 91 | + flipper[:other_feature].disable |
| 92 | + |
| 93 | + status, _, body = raw_api_get('/features', query) |
| 94 | + keys = body.fetch('features').map { |feature| feature.fetch('key') }.sort |
| 95 | + |
| 96 | + expect(status).to eq(200) |
| 97 | + expect(keys).to eq(expected_keys.sort) |
| 98 | + end |
| 99 | + end |
| 100 | + |
7 | 101 | context 'with flipper features' do |
8 | 102 | before do |
9 | 103 | flipper[:my_feature].enable |
|
80 | 174 | expect(json_response).to eq(expected_response) |
81 | 175 | end |
82 | 176 |
|
| 177 | + it 'responds without gates when instructed by param' do |
| 178 | + get '/features', 'exclude_gates' => 'true' |
| 179 | + |
| 180 | + expect(last_response.status).to eq(200) |
| 181 | + expect(json_response).to eq( |
| 182 | + 'features' => [ |
| 183 | + { |
| 184 | + 'key' => 'my_feature', |
| 185 | + 'state' => 'on', |
| 186 | + }, |
| 187 | + ] |
| 188 | + ) |
| 189 | + end |
| 190 | + |
83 | 191 | it 'ignores a leading empty query segment' do |
84 | 192 | get '/features?exclude_gate_names=true' |
85 | 193 | expected_response = json_response |
|
0 commit comments