Easier feature flag handling for Cypress (#16379)
* Add RailsEnvConstraint for routes * Add feature flags API * Add Cypress commands * Add show action, update commands, add e2e test * Update cypress/integration/seededFlows/toggleFeatureFlags.spec.js Co-authored-by: Suzanne Aitchison <suzanne@forem.com> * Move helper from command to test file * Update documentation Co-authored-by: Suzanne Aitchison <suzanne@forem.com>
This commit is contained in:
parent
14a945326d
commit
a1eb6358db
7 changed files with 166 additions and 0 deletions
27
app/controllers/api/v0/feature_flags_controller.rb
Normal file
27
app/controllers/api/v0/feature_flags_controller.rb
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
module Api
|
||||
module V0
|
||||
# This controller is used for toggling feature flags in the test
|
||||
# environment, specifically for Cypress tests.
|
||||
#
|
||||
# @note: Despite the used methods this controller does not add or remove
|
||||
# the flags themselves, I just wanted distinct methods for enabling and
|
||||
# disabling so we don't need conditional or boolean casting and these were
|
||||
# the most fitting actions.
|
||||
class FeatureFlagsController < ApiController
|
||||
def create
|
||||
FeatureFlag.enable(params[:flag])
|
||||
head :ok
|
||||
end
|
||||
|
||||
def show
|
||||
flag = params[:flag]
|
||||
render json: { flag => FeatureFlag.enabled?(flag) }
|
||||
end
|
||||
|
||||
def destroy
|
||||
FeatureFlag.disable(params[:flag])
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
18
app/lib/rails_env_constraint.rb
Normal file
18
app/lib/rails_env_constraint.rb
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Used as routing constraint to expose routes only in certain environments.
|
||||
class RailsEnvConstraint
|
||||
# @param [Array<string>] allowed_envs the environments the route(s) will be
|
||||
# available in.
|
||||
def initialize(allowed_envs:)
|
||||
@allowed_envs = allowed_envs
|
||||
end
|
||||
|
||||
# Returns true if we're in an allowed env
|
||||
#
|
||||
# @note We always ignore the request argument since it's not used.
|
||||
# @return [Boolean]
|
||||
def matches?(_req = nil)
|
||||
# NOTE: ActiveSupport::StringInquirer works with all string methods, so
|
||||
# e.g. "test" == "test".inquiry works as expected.
|
||||
@allowed_envs.any?(Rails.env)
|
||||
end
|
||||
end
|
||||
|
|
@ -84,6 +84,10 @@ Rails.application.routes.draw do
|
|||
resources :articles, only: [:index], to: "organizations#articles"
|
||||
end
|
||||
resource :instance, only: %i[show]
|
||||
|
||||
constraints(RailsEnvConstraint.new(allowed_envs: %w[test])) do
|
||||
resource :feature_flags, only: %i[create show destroy], param: :flag
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
18
cypress/integration/seededFlows/toggleFeatureFlags.spec.js
Normal file
18
cypress/integration/seededFlows/toggleFeatureFlags.spec.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
describe('Toggling feature flags', () => {
|
||||
function checkFeatureFlag(flag, expected) {
|
||||
return cy
|
||||
.request('GET', `/api/feature_flags?flag=${flag}`)
|
||||
.should((response) => {
|
||||
expect(response.body).to.deep.equal({ [flag]: expected });
|
||||
});
|
||||
}
|
||||
|
||||
it('toggles and verifies feature flags', () => {
|
||||
const flag = 'test_feature_flag';
|
||||
checkFeatureFlag(flag, false);
|
||||
cy.enableFeatureFlag(flag);
|
||||
checkFeatureFlag(flag, true);
|
||||
cy.disableFeatureFlag(flag);
|
||||
checkFeatureFlag(flag, false);
|
||||
});
|
||||
});
|
||||
|
|
@ -342,3 +342,25 @@ Cypress.Commands.add('createResponseTemplate', ({ title, content }) => {
|
|||
`utf8=%E2%9C%93&response_template%5Btitle%5D=${encodedTitle}&response_template%5Bcontent%5D=${encodedContent}`,
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Enables a feature flag.
|
||||
*
|
||||
* @param {string} flag The name of the feature flag to enable.
|
||||
*
|
||||
* @returns {Cypress.Chainable<Cypress.Response>} A cypress request for enabling a feature flag.
|
||||
*/
|
||||
Cypress.Commands.add('enableFeatureFlag', (flag) => {
|
||||
return cy.request('POST', '/api/feature_flags', { flag });
|
||||
});
|
||||
|
||||
/**
|
||||
* Disables a feature flag.
|
||||
*
|
||||
* @param {string} flag The name of the feature flag to disable.
|
||||
*
|
||||
* @returns {Cypress.Chainable<Cypress.Response>} A cypress request for disabling a feature flag.
|
||||
*/
|
||||
Cypress.Commands.add('disableFeatureFlag', (flag) => {
|
||||
return cy.request('DELETE', `/api/feature_flags?flag=${flag}`);
|
||||
});
|
||||
|
|
|
|||
13
spec/lib/rails_env_constraint_spec.rb
Normal file
13
spec/lib/rails_env_constraint_spec.rb
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe RailsEnvConstraint, type: :lib do
|
||||
it "matches only in the correct environments", :aggregate_failures do
|
||||
constraint = described_class.new(allowed_envs: %w[development])
|
||||
|
||||
allow(Rails).to receive(:env).and_return("development")
|
||||
expect(constraint.matches?).to be true
|
||||
|
||||
allow(Rails).to receive(:env).and_return("production")
|
||||
expect(constraint.matches?).to be false
|
||||
end
|
||||
end
|
||||
64
spec/requests/api/v0/feature_flags_spec.rb
Normal file
64
spec/requests/api/v0/feature_flags_spec.rb
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe "Api::V0::FeatureFlagsController", type: :request do
|
||||
let(:flag) { "test_flag" }
|
||||
let(:params) { { flag: flag } }
|
||||
|
||||
it "is not available in the production environment" do
|
||||
# We really need an ActiveSupport::StringInquirer here
|
||||
# rubocop:disable Rails/Inquiry
|
||||
allow(Rails).to receive(:env).and_return("production".inquiry)
|
||||
# rubocop:enable Rails/Inquiry
|
||||
|
||||
expect do
|
||||
post api_feature_flags_path, params: params
|
||||
end.to raise_error(ActionController::RoutingError)
|
||||
end
|
||||
|
||||
context "when toggling feature flags" do
|
||||
before { FeatureFlag.add(flag) }
|
||||
|
||||
after { FeatureFlag.remove(flag) }
|
||||
|
||||
it "can enable a disabled feature flag" do
|
||||
FeatureFlag.disable(flag)
|
||||
|
||||
expect do
|
||||
post api_feature_flags_path, params: params
|
||||
end.to change { FeatureFlag.enabled?(flag) }.from(false).to(true)
|
||||
end
|
||||
|
||||
it "keeps the flag enabled when it was already enabled" do
|
||||
FeatureFlag.enable(flag)
|
||||
|
||||
expect do
|
||||
post api_feature_flags_path, params: params
|
||||
end.not_to change { FeatureFlag.enabled?(flag) }.from(true)
|
||||
end
|
||||
|
||||
it "can disable an enabled feature flag" do
|
||||
FeatureFlag.enable(flag)
|
||||
|
||||
expect do
|
||||
delete api_feature_flags_path, params: params
|
||||
end.to change { FeatureFlag.enabled?(flag) }.from(true).to(false)
|
||||
end
|
||||
|
||||
it "keeps the flag disabled when it was already disabled" do
|
||||
FeatureFlag.disable(flag)
|
||||
|
||||
expect do
|
||||
delete api_feature_flags_path, params: params
|
||||
end.not_to change { FeatureFlag.enabled?(flag) }.from(false)
|
||||
end
|
||||
|
||||
it "shows the current value of a feature flag" do
|
||||
FeatureFlag.enable(flag)
|
||||
|
||||
get api_feature_flags_path(flag: flag)
|
||||
|
||||
parsed_response = JSON.parse(response.body)
|
||||
expect(parsed_response[flag]).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue