diff --git a/packages/core/src/api/app/FeatureFlags.test.tsx b/packages/core/src/api/app/FeatureFlags.test.tsx new file mode 100644 index 0000000000..2159d05e65 --- /dev/null +++ b/packages/core/src/api/app/FeatureFlags.test.tsx @@ -0,0 +1,155 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useContext } from 'react'; +import { render } from '@testing-library/react'; +import { + FeatureFlags, + FeatureFlagsContext, + FeatureFlagsContextProvider, +} from './FeatureFlags'; +import { FeatureFlagState } from '../apis/definitions/featureFlags'; + +describe('FeatureFlags', () => { + beforeEach(() => { + window.localStorage.clear(); + }); + + describe('#get', () => { + it('defaults to .NotEnabled', () => { + expect(FeatureFlags.get('enable-feature-flag')).toBe( + FeatureFlagState.NotEnabled, + ); + }); + + it('returns a .Enabled state', () => { + FeatureFlags.set('enable-feature-flag', FeatureFlagState.Enabled); + expect(FeatureFlags.get('enable-feature-flag')).toBe( + FeatureFlagState.Enabled, + ); + }); + + it('returns a .NotEnabled state', () => { + FeatureFlags.set('enable-feature-flag', FeatureFlagState.NotEnabled); + expect(FeatureFlags.get('enable-feature-flag')).toBe( + FeatureFlagState.NotEnabled, + ); + }); + }); + + describe('#set', () => { + it('fails if name is less than three characters', () => { + expect(() => { + FeatureFlags.set('ab', FeatureFlagState.NotEnabled); + }).toThrow(/minimum length of three characters/i); + }); + + it('fails if name is greater than 150 characters', () => { + expect(() => { + FeatureFlags.set( + 'loremipsumdolorsitametconsecteturadipiscingelitnuncvitaeportaexaullamcorperturpismaurisutmattisnequemorbisediaculisauguevivamuspulvinarcursuseratblandithendreritquisqueuttinciduntmagnavestibulumblanditaugueat', + FeatureFlagState.NotEnabled, + ); + }).toThrow(/not exceed 150 characters/i); + }); + + it('fails if name does not start with a lowercase letter', () => { + expect(() => { + FeatureFlags.set('123456789', FeatureFlagState.NotEnabled); + }).toThrow(/start with a lowercase letter/i); + }); + + it('fails if name contains characters other than lowercase letters, numbers and hyphens', () => { + expect(() => { + FeatureFlags.set('Invalid_Feature_Flag', FeatureFlagState.NotEnabled); + }).toThrow(/only contain lowercase letters, numbers and hyphens/i); + }); + + it('fails if state is not recognized from FeatureFlagState', () => { + expect(() => { + // @ts-ignore + FeatureFlags.set('valid-feature-flag', 'invalid state'); + }).toThrow(/requires a recognized value from the FeatureFlagState/i); + }); + + it('sets a feature flag to enabled', () => { + expect(window.localStorage.featureFlags).toBeUndefined(); + FeatureFlags.set('valid-feature-flag', FeatureFlagState.Enabled); + expect(window.localStorage.featureFlags).toBe( + '{"valid-feature-flag":true}', + ); + }); + + it('sets a feature flag to disabled', () => { + expect(window.localStorage.featureFlags).toBeUndefined(); + FeatureFlags.set('valid-feature-flag', FeatureFlagState.NotEnabled); + expect(window.localStorage.featureFlags).toBe('{}'); + }); + + it('sets a feature flag to disabled then enabled', () => { + expect(window.localStorage.featureFlags).toBeUndefined(); + FeatureFlags.set('valid-feature-flag', FeatureFlagState.NotEnabled); + FeatureFlags.set('valid-feature-flag', FeatureFlagState.Enabled); + expect(window.localStorage.featureFlags).toBe( + '{"valid-feature-flag":true}', + ); + }); + + it('sets multiple feature flags', () => { + expect(window.localStorage.featureFlags).toBeUndefined(); + FeatureFlags.set('valid-feature-flag', FeatureFlagState.Enabled); + FeatureFlags.set('another-valid-feature-flag', FeatureFlagState.Enabled); + expect(window.localStorage.featureFlags).toBe( + '{"valid-feature-flag":true,"another-valid-feature-flag":true}', + ); + }); + }); +}); + +describe('FeatureFlagsContext', () => { + it('returns an empty list without the context', () => { + expect.assertions(1); + + const Component = () => { + const { registeredFeatureFlags } = useContext(FeatureFlagsContext); + expect(registeredFeatureFlags).toEqual([]); + return null; + }; + + render(); + }); + + it('returns a list of registered feature flags', () => { + expect.assertions(1); + + const mockFeatureFlags = [ + { name: 'feature-flag-one', pluginId: 'plugin-one' }, + { name: 'feature-flag-two', pluginId: 'plugin-two' }, + ]; + + const Component = () => { + const { registeredFeatureFlags } = useContext(FeatureFlagsContext); + expect(registeredFeatureFlags).toBe(mockFeatureFlags); + return null; + }; + + render( + + + , + ); + }); +}); diff --git a/packages/core/src/api/app/FeatureFlags.tsx b/packages/core/src/api/app/FeatureFlags.tsx index 74060beb6d..2b543db216 100644 --- a/packages/core/src/api/app/FeatureFlags.tsx +++ b/packages/core/src/api/app/FeatureFlags.tsx @@ -56,8 +56,34 @@ class FeatureFlagsImpl implements FeatureFlagsApi { set(name: FeatureFlagName, state: FeatureFlagState): void { const flags = this.getUserEnabledFeatureFlags(); - if (state === FeatureFlagState.NotEnabled) flags.delete(name); - if (state === FeatureFlagState.Enabled) flags.add(name); + if (name.length < 3) { + throw new Error( + 'The `name` argument must have a minimum length of three characters.', + ); + } + + if (name.length > 150) { + throw new Error('The `name` argument must not exceed 150 characters.'); + } + + if (!name.match(/^[a-z]+[a-z0-9-]+$/)) { + throw new Error( + 'The `name` argument must start with a lowercase letter and only contain lowercase letters, numbers and hyphens.' + + 'Examples: feature-flag-one, alpha, release-2020', + ); + } + + if (state === FeatureFlagState.Enabled) { + flags.add(name); + } else if (state === FeatureFlagState.NotEnabled) { + flags.delete(name); + } else { + throw new Error( + 'The `state` argument requires a recognized value from the FeatureFlagState enum. ' + + 'Please check the Backstage documentation to see all the available options.' + + 'Example values: FeatureFlagState.NotEnabled, FeatureFlagState.Enabled', + ); + } window.localStorage.setItem( this.localStorageKey,