require 'spec_helper' describe API::Unleash do include FeatureFlagHelpers set(:project) { create(:project) } let(:project_id) { project.id } let(:feature_enabled) { true } let(:params) { } let(:headers) { } before do stub_licensed_features(feature_flags: feature_enabled) end shared_examples 'authenticated request' do context 'when using instance id' do let(:client) { create(:operations_feature_flags_client, project: project) } let(:params) { { instance_id: client.token } } it 'responds with OK' do subject expect(response).to have_gitlab_http_status(200) end context 'when feature is not available' do let(:feature_enabled) { false } it 'responds with forbidden' do subject expect(response).to have_gitlab_http_status(403) end end end context 'when using header' do let(:client) { create(:operations_feature_flags_client, project: project) } let(:headers) { { "UNLEASH-INSTANCEID" => client.token }} it 'responds with OK' do subject expect(response).to have_gitlab_http_status(200) end end context 'when using bogus instance id' do let(:params) { { instance_id: 'token' } } it 'responds with unauthorized' do subject expect(response).to have_gitlab_http_status(401) end end context 'when using not existing project' do let(:project_id) { -5000 } let(:params) { { instance_id: 'token' } } it 'responds with unauthorized' do subject expect(response).to have_gitlab_http_status(401) end end end shared_examples_for 'support multiple environments' do let(:client) { create(:operations_feature_flags_client, project: project) } let(:base_headers) { { "UNLEASH-INSTANCEID" => client.token } } let(:headers) { base_headers.merge({ "UNLEASH-APPNAME" => "test" }) } let!(:feature_flag_1) do create(:operations_feature_flag, name: "feature_flag_1", project: project, active: true) end let!(:feature_flag_2) do create(:operations_feature_flag, name: "feature_flag_2", project: project, active: false) end before do create_scope(feature_flag_1, 'production', false) create_scope(feature_flag_2, 'review/*', true) end it 'does not have N+1 problem' do recorded = ActiveRecord::QueryRecorder.new { subject } expect(recorded.count).to be_within(8).of(10) end context 'when app name is staging' do let(:headers) { base_headers.merge({ "UNLEASH-APPNAME" => "staging" }) } it 'returns correct active values' do subject feature_flag_1 = json_response['features'].select { |f| f['name'] == 'feature_flag_1' }.first feature_flag_2 = json_response['features'].select { |f| f['name'] == 'feature_flag_2' }.first expect(feature_flag_1['enabled']).to eq(true) expect(feature_flag_2['enabled']).to eq(false) end end context 'when app name is production' do let(:headers) { base_headers.merge({ "UNLEASH-APPNAME" => "production" }) } it 'returns correct active values' do subject feature_flag_1 = json_response['features'].select { |f| f['name'] == 'feature_flag_1' }.first feature_flag_2 = json_response['features'].select { |f| f['name'] == 'feature_flag_2' }.first expect(feature_flag_1['enabled']).to eq(false) expect(feature_flag_2['enabled']).to eq(false) end end context 'when app name is review/patch-1' do let(:headers) { base_headers.merge({ "UNLEASH-APPNAME" => "review/patch-1" }) } it 'returns correct active values' do subject feature_flag_1 = json_response['features'].select { |f| f['name'] == 'feature_flag_1' }.first feature_flag_2 = json_response['features'].select { |f| f['name'] == 'feature_flag_2' }.first expect(feature_flag_1['enabled']).to eq(true) expect(feature_flag_2['enabled']).to eq(true) end end context 'when app name is empty' do let(:headers) { base_headers } it 'returns empty list' do subject expect(json_response['features'].count).to eq(0) end end end %w(/feature_flags/unleash/:project_id/features /feature_flags/unleash/:project_id/client/features).each do |features_endpoint| describe "GET #{features_endpoint}" do let(:features_url) { features_endpoint.sub(':project_id', project_id.to_s) } subject { get api(features_url), params: params, headers: headers } it_behaves_like 'authenticated request' it_behaves_like 'support multiple environments' context 'with a list of feature flag' do let(:client) { create(:operations_feature_flags_client, project: project) } let(:headers) { { "UNLEASH-INSTANCEID" => client.token, "UNLEASH-APPNAME" => "production" }} let!(:enable_feature_flag) { create(:operations_feature_flag, project: project, name: 'feature1', active: true) } let!(:disabled_feature_flag) { create(:operations_feature_flag, project: project, name: 'feature2', active: false) } it 'responds with a list of features' do subject expect(response).to have_gitlab_http_status(200) expect(json_response['version']).to eq(1) expect(json_response['features']).not_to be_empty expect(json_response['features'].map { |f| f['name'] }.sort).to eq(%w[feature1 feature2]) end it 'matches json schema' do subject expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('unleash/unleash', dir: 'ee') end end it 'returns a feature flag strategy' do client = create(:operations_feature_flags_client, project: project) feature_flag = create(:operations_feature_flag, project: project, name: 'feature1', active: true) create(:operations_feature_flag_scope, feature_flag: feature_flag, environment_scope: 'sandbox', active: true, strategies: [{ name: "gradualRolloutUserId", parameters: { groupId: "default", percentage: "50" } }]) headers = { "UNLEASH-INSTANCEID" => client.token, "UNLEASH-APPNAME" => "sandbox" } get api(features_url), headers: headers expect(response).to have_gitlab_http_status(:ok) strategies = json_response['features'].first['strategies'] expect(strategies).to eq([{ "name" => "gradualRolloutUserId", "parameters" => { "percentage" => "50", "groupId" => "default" } }]) end it 'returns a default strategy for a scope' do client = create(:operations_feature_flags_client, project: project) feature_flag = create(:operations_feature_flag, project: project, name: 'feature1', active: true) create(:operations_feature_flag_scope, feature_flag: feature_flag, environment_scope: 'sandbox', active: true) headers = { "UNLEASH-INSTANCEID" => client.token, "UNLEASH-APPNAME" => "sandbox" } get api(features_url), headers: headers expect(response).to have_gitlab_http_status(:ok) strategies = json_response['features'].first['strategies'] expect(strategies).to eq([{ "name" => "default", "parameters" => {} }]) end it 'returns multiple strategies for a feature flag' do client = create(:operations_feature_flags_client, project: project) feature_flag = create(:operations_feature_flag, project: project, name: 'feature1', active: true) create(:operations_feature_flag_scope, feature_flag: feature_flag, environment_scope: 'staging', active: true, strategies: [{ name: "userWithId", parameters: { userIds: "max,fred" } }, { name: "gradualRolloutUserId", parameters: { groupId: "default", percentage: "50" } }]) headers = { "UNLEASH-INSTANCEID" => client.token, "UNLEASH-APPNAME" => "staging" } get api(features_url), headers: headers expect(response).to have_gitlab_http_status(:ok) strategies = json_response['features'].first['strategies'].sort_by { |s| s['name'] } expect(strategies).to eq([{ "name" => "gradualRolloutUserId", "parameters" => { "percentage" => "50", "groupId" => "default" } }, { "name" => "userWithId", "parameters" => { "userIds" => "max,fred" } }]) end end end describe 'POST /feature_flags/unleash/:project_id/client/register' do subject { post api("/feature_flags/unleash/#{project_id}/client/register"), params: params, headers: headers } it_behaves_like 'authenticated request' end describe 'POST /feature_flags/unleash/:project_id/client/metrics' do subject { post api("/feature_flags/unleash/#{project_id}/client/metrics"), params: params, headers: headers } it_behaves_like 'authenticated request' end end