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:
Michael Kohl 2022-02-03 22:42:13 +07:00 committed by GitHub
parent 14a945326d
commit a1eb6358db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 166 additions and 0 deletions

View 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

View 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

View file

@ -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

View 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);
});
});

View file

@ -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}`);
});

View 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

View 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