diff options
Diffstat (limited to 'spec/frontend/feature_flags/store')
-rw-r--r-- | spec/frontend/feature_flags/store/edit/actions_spec.js | 334 | ||||
-rw-r--r-- | spec/frontend/feature_flags/store/edit/mutations_spec.js | 150 | ||||
-rw-r--r-- | spec/frontend/feature_flags/store/helpers_spec.js | 514 | ||||
-rw-r--r-- | spec/frontend/feature_flags/store/index/actions_spec.js | 605 | ||||
-rw-r--r-- | spec/frontend/feature_flags/store/index/mutations_spec.js | 332 | ||||
-rw-r--r-- | spec/frontend/feature_flags/store/new/actions_spec.js | 223 | ||||
-rw-r--r-- | spec/frontend/feature_flags/store/new/mutations_spec.js | 65 |
7 files changed, 2223 insertions, 0 deletions
diff --git a/spec/frontend/feature_flags/store/edit/actions_spec.js b/spec/frontend/feature_flags/store/edit/actions_spec.js new file mode 100644 index 00000000000..4f20b9713bf --- /dev/null +++ b/spec/frontend/feature_flags/store/edit/actions_spec.js @@ -0,0 +1,334 @@ +import MockAdapter from 'axios-mock-adapter'; +import testAction from 'helpers/vuex_action_helper'; +import { TEST_HOST } from 'spec/test_constants'; +import { + setEndpoint, + setPath, + updateFeatureFlag, + requestUpdateFeatureFlag, + receiveUpdateFeatureFlagSuccess, + receiveUpdateFeatureFlagError, + fetchFeatureFlag, + requestFeatureFlag, + receiveFeatureFlagSuccess, + receiveFeatureFlagError, + toggleActive, +} from '~/feature_flags/store/modules/edit/actions'; +import state from '~/feature_flags/store/modules/edit/state'; +import { + mapStrategiesToRails, + mapFromScopesViewModel, +} from '~/feature_flags/store/modules/helpers'; +import { + NEW_VERSION_FLAG, + LEGACY_FLAG, + ROLLOUT_STRATEGY_ALL_USERS, +} from '~/feature_flags/constants'; +import * as types from '~/feature_flags/store/modules/edit/mutation_types'; +import axios from '~/lib/utils/axios_utils'; + +jest.mock('~/lib/utils/url_utility'); + +describe('Feature flags Edit Module actions', () => { + let mockedState; + + beforeEach(() => { + mockedState = state(); + }); + + describe('setEndpoint', () => { + it('should commit SET_ENDPOINT mutation', done => { + testAction( + setEndpoint, + 'feature_flags.json', + mockedState, + [{ type: types.SET_ENDPOINT, payload: 'feature_flags.json' }], + [], + done, + ); + }); + }); + + describe('setPath', () => { + it('should commit SET_PATH mutation', done => { + testAction( + setPath, + '/feature_flags', + mockedState, + [{ type: types.SET_PATH, payload: '/feature_flags' }], + [], + done, + ); + }); + }); + + describe('updateFeatureFlag', () => { + let mock; + + beforeEach(() => { + mockedState.endpoint = `${TEST_HOST}/endpoint.json`; + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('success', () => { + it('dispatches requestUpdateFeatureFlag and receiveUpdateFeatureFlagSuccess ', done => { + const featureFlag = { + name: 'feature_flag', + description: 'feature flag', + scopes: [ + { + id: '1', + environmentScope: '*', + active: true, + shouldBeDestroyed: false, + canUpdate: true, + protected: false, + rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS, + }, + ], + version: LEGACY_FLAG, + active: true, + }; + mock.onPut(mockedState.endpoint, mapFromScopesViewModel(featureFlag)).replyOnce(200); + + testAction( + updateFeatureFlag, + featureFlag, + mockedState, + [], + [ + { + type: 'requestUpdateFeatureFlag', + }, + { + type: 'receiveUpdateFeatureFlagSuccess', + }, + ], + done, + ); + }); + it('handles new version flags as well', done => { + const featureFlag = { + name: 'name', + description: 'description', + active: true, + version: NEW_VERSION_FLAG, + strategies: [ + { + name: ROLLOUT_STRATEGY_ALL_USERS, + parameters: {}, + id: 1, + scopes: [{ id: 1, environmentScope: 'environmentScope', shouldBeDestroyed: false }], + shouldBeDestroyed: false, + }, + ], + }; + mock.onPut(mockedState.endpoint, mapStrategiesToRails(featureFlag)).replyOnce(200); + + testAction( + updateFeatureFlag, + featureFlag, + mockedState, + [], + [ + { + type: 'requestUpdateFeatureFlag', + }, + { + type: 'receiveUpdateFeatureFlagSuccess', + }, + ], + done, + ); + }); + }); + + describe('error', () => { + it('dispatches requestUpdateFeatureFlag and receiveUpdateFeatureFlagError ', done => { + mock.onPut(`${TEST_HOST}/endpoint.json`).replyOnce(500, { message: [] }); + + testAction( + updateFeatureFlag, + { + name: 'feature_flag', + description: 'feature flag', + scopes: [{ environment_scope: '*', active: true }], + }, + mockedState, + [], + [ + { + type: 'requestUpdateFeatureFlag', + }, + { + type: 'receiveUpdateFeatureFlagError', + payload: { message: [] }, + }, + ], + done, + ); + }); + }); + }); + + describe('requestUpdateFeatureFlag', () => { + it('should commit REQUEST_UPDATE_FEATURE_FLAG mutation', done => { + testAction( + requestUpdateFeatureFlag, + null, + mockedState, + [{ type: types.REQUEST_UPDATE_FEATURE_FLAG }], + [], + done, + ); + }); + }); + + describe('receiveUpdateFeatureFlagSuccess', () => { + it('should commit RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS mutation', done => { + testAction( + receiveUpdateFeatureFlagSuccess, + null, + mockedState, + [ + { + type: types.RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS, + }, + ], + [], + done, + ); + }); + }); + + describe('receiveUpdateFeatureFlagError', () => { + it('should commit RECEIVE_UPDATE_FEATURE_FLAG_ERROR mutation', done => { + testAction( + receiveUpdateFeatureFlagError, + 'There was an error', + mockedState, + [{ type: types.RECEIVE_UPDATE_FEATURE_FLAG_ERROR, payload: 'There was an error' }], + [], + done, + ); + }); + }); + + describe('fetchFeatureFlag', () => { + let mock; + + beforeEach(() => { + mockedState.endpoint = `${TEST_HOST}/endpoint.json`; + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('success', () => { + it('dispatches requestFeatureFlag and receiveFeatureFlagSuccess ', done => { + mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, { id: 1 }); + + testAction( + fetchFeatureFlag, + { id: 1 }, + mockedState, + [], + [ + { + type: 'requestFeatureFlag', + }, + { + type: 'receiveFeatureFlagSuccess', + payload: { id: 1 }, + }, + ], + done, + ); + }); + }); + + describe('error', () => { + it('dispatches requestFeatureFlag and receiveUpdateFeatureFlagError ', done => { + mock.onGet(`${TEST_HOST}/endpoint.json`, {}).replyOnce(500, {}); + + testAction( + fetchFeatureFlag, + null, + mockedState, + [], + [ + { + type: 'requestFeatureFlag', + }, + { + type: 'receiveFeatureFlagError', + }, + ], + done, + ); + }); + }); + }); + + describe('requestFeatureFlag', () => { + it('should commit REQUEST_FEATURE_FLAG mutation', done => { + testAction( + requestFeatureFlag, + null, + mockedState, + [{ type: types.REQUEST_FEATURE_FLAG }], + [], + done, + ); + }); + }); + + describe('receiveFeatureFlagSuccess', () => { + it('should commit RECEIVE_FEATURE_FLAG_SUCCESS mutation', done => { + testAction( + receiveFeatureFlagSuccess, + { id: 1 }, + mockedState, + [{ type: types.RECEIVE_FEATURE_FLAG_SUCCESS, payload: { id: 1 } }], + [], + done, + ); + }); + }); + + describe('receiveFeatureFlagError', () => { + it('should commit RECEIVE_FEATURE_FLAG_ERROR mutation', done => { + testAction( + receiveFeatureFlagError, + null, + mockedState, + [ + { + type: types.RECEIVE_FEATURE_FLAG_ERROR, + }, + ], + [], + done, + ); + }); + }); + + describe('toggelActive', () => { + it('should commit TOGGLE_ACTIVE mutation', done => { + testAction( + toggleActive, + true, + mockedState, + [{ type: types.TOGGLE_ACTIVE, payload: true }], + [], + done, + ); + }); + }); +}); diff --git a/spec/frontend/feature_flags/store/edit/mutations_spec.js b/spec/frontend/feature_flags/store/edit/mutations_spec.js new file mode 100644 index 00000000000..21d4e962b48 --- /dev/null +++ b/spec/frontend/feature_flags/store/edit/mutations_spec.js @@ -0,0 +1,150 @@ +import state from '~/feature_flags/store/modules/edit/state'; +import mutations from '~/feature_flags/store/modules/edit/mutations'; +import * as types from '~/feature_flags/store/modules/edit/mutation_types'; + +describe('Feature flags Edit Module Mutations', () => { + let stateCopy; + + beforeEach(() => { + stateCopy = state(); + }); + + describe('SET_ENDPOINT', () => { + it('should set endpoint', () => { + mutations[types.SET_ENDPOINT](stateCopy, 'feature_flags.json'); + + expect(stateCopy.endpoint).toEqual('feature_flags.json'); + }); + }); + + describe('SET_PATH', () => { + it('should set provided options', () => { + mutations[types.SET_PATH](stateCopy, 'feature_flags'); + + expect(stateCopy.path).toEqual('feature_flags'); + }); + }); + + describe('REQUEST_FEATURE_FLAG', () => { + it('should set isLoading to true', () => { + mutations[types.REQUEST_FEATURE_FLAG](stateCopy); + + expect(stateCopy.isLoading).toEqual(true); + }); + + it('should set error to an empty array', () => { + mutations[types.REQUEST_FEATURE_FLAG](stateCopy); + + expect(stateCopy.error).toEqual([]); + }); + }); + + describe('RECEIVE_FEATURE_FLAG_SUCCESS', () => { + const data = { + name: '*', + description: 'All environments', + scopes: [{ id: 1 }], + iid: 5, + version: 'new_version_flag', + strategies: [ + { id: 1, scopes: [{ environment_scope: '*' }], name: 'default', parameters: {} }, + ], + }; + + beforeEach(() => { + mutations[types.RECEIVE_FEATURE_FLAG_SUCCESS](stateCopy, data); + }); + + it('should set isLoading to false', () => { + expect(stateCopy.isLoading).toEqual(false); + }); + + it('should set hasError to false', () => { + expect(stateCopy.hasError).toEqual(false); + }); + + it('should set name with the provided one', () => { + expect(stateCopy.name).toEqual(data.name); + }); + + it('should set description with the provided one', () => { + expect(stateCopy.description).toEqual(data.description); + }); + + it('should set scope with the provided one', () => { + expect(stateCopy.scope).toEqual(data.scope); + }); + + it('should set the iid to the provided one', () => { + expect(stateCopy.iid).toEqual(data.iid); + }); + + it('should set the version to the provided one', () => { + expect(stateCopy.version).toBe('new_version_flag'); + }); + + it('should set the strategies to the provided one', () => { + expect(stateCopy.strategies).toEqual([ + { + id: 1, + scopes: [{ environmentScope: '*', shouldBeDestroyed: false }], + name: 'default', + parameters: {}, + shouldBeDestroyed: false, + }, + ]); + }); + }); + + describe('RECEIVE_FEATURE_FLAG_ERROR', () => { + beforeEach(() => { + mutations[types.RECEIVE_FEATURE_FLAG_ERROR](stateCopy); + }); + + it('should set isLoading to false', () => { + expect(stateCopy.isLoading).toEqual(false); + }); + + it('should set hasError to true', () => { + expect(stateCopy.hasError).toEqual(true); + }); + }); + + describe('REQUEST_UPDATE_FEATURE_FLAG', () => { + beforeEach(() => { + mutations[types.REQUEST_UPDATE_FEATURE_FLAG](stateCopy); + }); + + it('should set isSendingRequest to true', () => { + expect(stateCopy.isSendingRequest).toEqual(true); + }); + + it('should set error to an empty array', () => { + expect(stateCopy.error).toEqual([]); + }); + }); + + describe('RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS', () => { + it('should set isSendingRequest to false', () => { + mutations[types.RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS](stateCopy); + + expect(stateCopy.isSendingRequest).toEqual(false); + }); + }); + + describe('RECEIVE_UPDATE_FEATURE_FLAG_ERROR', () => { + beforeEach(() => { + mutations[types.RECEIVE_UPDATE_FEATURE_FLAG_ERROR](stateCopy, { + message: ['Name is required'], + }); + }); + + it('should set isSendingRequest to false', () => { + expect(stateCopy.isSendingRequest).toEqual(false); + }); + + it('should set error to the given message', () => { + expect(stateCopy.error).toEqual(['Name is required']); + }); + }); +}); diff --git a/spec/frontend/feature_flags/store/helpers_spec.js b/spec/frontend/feature_flags/store/helpers_spec.js new file mode 100644 index 00000000000..0bc15ab70aa --- /dev/null +++ b/spec/frontend/feature_flags/store/helpers_spec.js @@ -0,0 +1,514 @@ +import { uniqueId } from 'lodash'; +import { + mapToScopesViewModel, + mapFromScopesViewModel, + createNewEnvironmentScope, + mapStrategiesToViewModel, + mapStrategiesToRails, +} from '~/feature_flags/store/modules/helpers'; +import { + ROLLOUT_STRATEGY_ALL_USERS, + ROLLOUT_STRATEGY_PERCENT_ROLLOUT, + ROLLOUT_STRATEGY_USER_ID, + PERCENT_ROLLOUT_GROUP_ID, + INTERNAL_ID_PREFIX, + DEFAULT_PERCENT_ROLLOUT, + LEGACY_FLAG, + NEW_VERSION_FLAG, +} from '~/feature_flags/constants'; + +describe('feature flags helpers spec', () => { + describe('mapToScopesViewModel', () => { + it('converts the data object from the Rails API into something more usable by Vue', () => { + const input = [ + { + id: 3, + environment_scope: 'environment_scope', + active: true, + can_update: true, + protected: true, + strategies: [ + { + name: ROLLOUT_STRATEGY_PERCENT_ROLLOUT, + parameters: { + percentage: '56', + }, + }, + { + name: ROLLOUT_STRATEGY_USER_ID, + parameters: { + userIds: '123,234', + }, + }, + ], + + _destroy: true, + }, + ]; + + const expected = [ + expect.objectContaining({ + id: 3, + environmentScope: 'environment_scope', + active: true, + canUpdate: true, + protected: true, + rolloutStrategy: ROLLOUT_STRATEGY_PERCENT_ROLLOUT, + rolloutPercentage: '56', + rolloutUserIds: '123, 234', + shouldBeDestroyed: true, + }), + ]; + + const actual = mapToScopesViewModel(input); + + expect(actual).toEqual(expected); + }); + + it('returns Boolean properties even when their Rails counterparts were not provided (are `undefined`)', () => { + const input = [ + { + id: 3, + environment_scope: 'environment_scope', + }, + ]; + + const [result] = mapToScopesViewModel(input); + + expect(result).toEqual( + expect.objectContaining({ + active: false, + canUpdate: false, + protected: false, + shouldBeDestroyed: false, + }), + ); + }); + + it('returns an empty array if null or undefined is provided as a parameter', () => { + expect(mapToScopesViewModel(null)).toEqual([]); + expect(mapToScopesViewModel(undefined)).toEqual([]); + }); + + describe('with user IDs per environment', () => { + let oldGon; + + beforeEach(() => { + oldGon = window.gon; + window.gon = { features: { featureFlagsUsersPerEnvironment: true } }; + }); + + afterEach(() => { + window.gon = oldGon; + }); + + it('sets the user IDs as a comma separated string', () => { + const input = [ + { + id: 3, + environment_scope: 'environment_scope', + active: true, + can_update: true, + protected: true, + strategies: [ + { + name: ROLLOUT_STRATEGY_PERCENT_ROLLOUT, + parameters: { + percentage: '56', + }, + }, + { + name: ROLLOUT_STRATEGY_USER_ID, + parameters: { + userIds: '123,234', + }, + }, + ], + + _destroy: true, + }, + ]; + + const expected = [ + { + id: 3, + environmentScope: 'environment_scope', + active: true, + canUpdate: true, + protected: true, + rolloutStrategy: ROLLOUT_STRATEGY_PERCENT_ROLLOUT, + rolloutPercentage: '56', + rolloutUserIds: '123, 234', + shouldBeDestroyed: true, + shouldIncludeUserIds: true, + }, + ]; + + const actual = mapToScopesViewModel(input); + + expect(actual).toEqual(expected); + }); + }); + }); + + describe('mapFromScopesViewModel', () => { + it('converts the object emitted from the Vue component into an object than is in the right format to be submitted to the Rails API', () => { + const input = { + name: 'name', + description: 'description', + active: true, + scopes: [ + { + id: 4, + environmentScope: 'environmentScope', + active: true, + canUpdate: true, + protected: true, + shouldBeDestroyed: true, + shouldIncludeUserIds: true, + rolloutStrategy: ROLLOUT_STRATEGY_PERCENT_ROLLOUT, + rolloutPercentage: '48', + rolloutUserIds: '123, 234', + }, + ], + }; + + const expected = { + operations_feature_flag: { + name: 'name', + description: 'description', + active: true, + version: LEGACY_FLAG, + scopes_attributes: [ + { + id: 4, + environment_scope: 'environmentScope', + active: true, + can_update: true, + protected: true, + _destroy: true, + strategies: [ + { + name: ROLLOUT_STRATEGY_PERCENT_ROLLOUT, + parameters: { + groupId: PERCENT_ROLLOUT_GROUP_ID, + percentage: '48', + }, + }, + { + name: ROLLOUT_STRATEGY_USER_ID, + parameters: { + userIds: '123,234', + }, + }, + ], + }, + ], + }, + }; + + const actual = mapFromScopesViewModel(input); + + expect(actual).toEqual(expected); + }); + + it('should strip out internal IDs', () => { + const input = { + scopes: [{ id: 3 }, { id: uniqueId(INTERNAL_ID_PREFIX) }], + }; + + const result = mapFromScopesViewModel(input); + const [realId, internalId] = result.operations_feature_flag.scopes_attributes; + + expect(realId.id).toBe(3); + expect(internalId.id).toBeUndefined(); + }); + + it('returns scopes_attributes as [] if param.scopes is null or undefined', () => { + let { + operations_feature_flag: { scopes_attributes: actualScopes }, + } = mapFromScopesViewModel({ scopes: null }); + + expect(actualScopes).toEqual([]); + + ({ + operations_feature_flag: { scopes_attributes: actualScopes }, + } = mapFromScopesViewModel({ scopes: undefined })); + + expect(actualScopes).toEqual([]); + }); + describe('with user IDs per environment', () => { + it('sets the user IDs as a comma separated string', () => { + const input = { + name: 'name', + description: 'description', + active: true, + scopes: [ + { + id: 4, + environmentScope: 'environmentScope', + active: true, + canUpdate: true, + protected: true, + shouldBeDestroyed: true, + rolloutStrategy: ROLLOUT_STRATEGY_PERCENT_ROLLOUT, + rolloutPercentage: '48', + rolloutUserIds: '123, 234', + shouldIncludeUserIds: true, + }, + ], + }; + + const expected = { + operations_feature_flag: { + name: 'name', + description: 'description', + version: LEGACY_FLAG, + active: true, + scopes_attributes: [ + { + id: 4, + environment_scope: 'environmentScope', + active: true, + can_update: true, + protected: true, + _destroy: true, + strategies: [ + { + name: ROLLOUT_STRATEGY_PERCENT_ROLLOUT, + parameters: { + groupId: PERCENT_ROLLOUT_GROUP_ID, + percentage: '48', + }, + }, + { + name: ROLLOUT_STRATEGY_USER_ID, + parameters: { + userIds: '123,234', + }, + }, + ], + }, + ], + }, + }; + + const actual = mapFromScopesViewModel(input); + + expect(actual).toEqual(expected); + }); + }); + }); + + describe('createNewEnvironmentScope', () => { + it('should return a new environment scope object populated with the default options', () => { + const expected = { + environmentScope: '', + active: false, + id: expect.stringContaining(INTERNAL_ID_PREFIX), + rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS, + rolloutPercentage: DEFAULT_PERCENT_ROLLOUT, + rolloutUserIds: '', + }; + + const actual = createNewEnvironmentScope(); + + expect(actual).toEqual(expected); + }); + + it('should return a new environment scope object with overrides applied', () => { + const overrides = { + environmentScope: 'environmentScope', + active: true, + }; + + const expected = { + environmentScope: 'environmentScope', + active: true, + id: expect.stringContaining(INTERNAL_ID_PREFIX), + rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS, + rolloutPercentage: DEFAULT_PERCENT_ROLLOUT, + rolloutUserIds: '', + }; + + const actual = createNewEnvironmentScope(overrides); + + expect(actual).toEqual(expected); + }); + + it('sets canUpdate and protected when called with featureFlagPermissions=true', () => { + expect(createNewEnvironmentScope({}, true)).toEqual( + expect.objectContaining({ + canUpdate: true, + protected: false, + }), + ); + }); + }); + + describe('mapStrategiesToViewModel', () => { + it('should map rails casing to view model casing', () => { + expect( + mapStrategiesToViewModel([ + { + id: '1', + name: 'default', + parameters: {}, + scopes: [ + { + environment_scope: '*', + id: '1', + }, + ], + }, + ]), + ).toEqual([ + { + id: '1', + name: 'default', + parameters: {}, + shouldBeDestroyed: false, + scopes: [ + { + shouldBeDestroyed: false, + environmentScope: '*', + id: '1', + }, + ], + }, + ]); + }); + + it('inserts spaces between user ids', () => { + const strategy = mapStrategiesToViewModel([ + { + id: '1', + name: 'userWithId', + parameters: { userIds: 'user1,user2,user3' }, + scopes: [], + }, + ])[0]; + + expect(strategy.parameters).toEqual({ userIds: 'user1, user2, user3' }); + }); + }); + + describe('mapStrategiesToRails', () => { + it('should map rails casing to view model casing', () => { + expect( + mapStrategiesToRails({ + name: 'test', + description: 'test description', + version: NEW_VERSION_FLAG, + active: true, + strategies: [ + { + id: '1', + name: 'default', + parameters: {}, + shouldBeDestroyed: true, + scopes: [ + { + environmentScope: '*', + id: '1', + shouldBeDestroyed: true, + }, + ], + }, + ], + }), + ).toEqual({ + operations_feature_flag: { + name: 'test', + description: 'test description', + version: NEW_VERSION_FLAG, + active: true, + strategies_attributes: [ + { + id: '1', + name: 'default', + parameters: {}, + _destroy: true, + scopes_attributes: [ + { + environment_scope: '*', + id: '1', + _destroy: true, + }, + ], + }, + ], + }, + }); + }); + + it('should insert a default * scope if there are none', () => { + expect( + mapStrategiesToRails({ + name: 'test', + description: 'test description', + version: NEW_VERSION_FLAG, + active: true, + strategies: [ + { + id: '1', + name: 'default', + parameters: {}, + scopes: [], + }, + ], + }), + ).toEqual({ + operations_feature_flag: { + name: 'test', + description: 'test description', + version: NEW_VERSION_FLAG, + active: true, + strategies_attributes: [ + { + id: '1', + name: 'default', + parameters: {}, + scopes_attributes: [ + { + environment_scope: '*', + }, + ], + }, + ], + }, + }); + }); + + it('removes white space between user ids', () => { + const result = mapStrategiesToRails({ + name: 'test', + version: NEW_VERSION_FLAG, + active: true, + strategies: [ + { + id: '1', + name: 'userWithId', + parameters: { userIds: 'user1, user2, user3' }, + scopes: [], + }, + ], + }); + + const strategyAttrs = result.operations_feature_flag.strategies_attributes[0]; + + expect(strategyAttrs.parameters).toEqual({ userIds: 'user1,user2,user3' }); + }); + + it('preserves the value of active', () => { + const result = mapStrategiesToRails({ + name: 'test', + version: NEW_VERSION_FLAG, + active: false, + strategies: [], + }); + + expect(result.operations_feature_flag.active).toBe(false); + }); + }); +}); diff --git a/spec/frontend/feature_flags/store/index/actions_spec.js b/spec/frontend/feature_flags/store/index/actions_spec.js new file mode 100644 index 00000000000..0ada84aed33 --- /dev/null +++ b/spec/frontend/feature_flags/store/index/actions_spec.js @@ -0,0 +1,605 @@ +import MockAdapter from 'axios-mock-adapter'; +import testAction from 'helpers/vuex_action_helper'; +import { TEST_HOST } from 'spec/test_constants'; +import Api from '~/api'; +import { + requestFeatureFlags, + receiveFeatureFlagsSuccess, + receiveFeatureFlagsError, + fetchFeatureFlags, + setFeatureFlagsEndpoint, + setFeatureFlagsOptions, + setInstanceIdEndpoint, + setInstanceId, + rotateInstanceId, + requestRotateInstanceId, + receiveRotateInstanceIdSuccess, + receiveRotateInstanceIdError, + toggleFeatureFlag, + updateFeatureFlag, + receiveUpdateFeatureFlagSuccess, + receiveUpdateFeatureFlagError, + requestUserLists, + receiveUserListsSuccess, + receiveUserListsError, + fetchUserLists, + deleteUserList, + receiveDeleteUserListError, + clearAlert, +} from '~/feature_flags/store/modules/index/actions'; +import { mapToScopesViewModel } from '~/feature_flags/store/modules/helpers'; +import state from '~/feature_flags/store/modules/index/state'; +import * as types from '~/feature_flags/store/modules/index/mutation_types'; +import axios from '~/lib/utils/axios_utils'; +import { getRequestData, rotateData, featureFlag, userList } from '../../mock_data'; + +jest.mock('~/api.js'); + +describe('Feature flags actions', () => { + let mockedState; + + beforeEach(() => { + mockedState = state(); + }); + + describe('setFeatureFlagsEndpoint', () => { + it('should commit SET_FEATURE_FLAGS_ENDPOINT mutation', done => { + testAction( + setFeatureFlagsEndpoint, + 'feature_flags.json', + mockedState, + [{ type: types.SET_FEATURE_FLAGS_ENDPOINT, payload: 'feature_flags.json' }], + [], + done, + ); + }); + }); + + describe('setFeatureFlagsOptions', () => { + it('should commit SET_FEATURE_FLAGS_OPTIONS mutation', done => { + testAction( + setFeatureFlagsOptions, + { page: '1', scope: 'all' }, + mockedState, + [{ type: types.SET_FEATURE_FLAGS_OPTIONS, payload: { page: '1', scope: 'all' } }], + [], + done, + ); + }); + }); + + describe('setInstanceIdEndpoint', () => { + it('should commit SET_INSTANCE_ID_ENDPOINT mutation', done => { + testAction( + setInstanceIdEndpoint, + 'instance_id.json', + mockedState, + [{ type: types.SET_INSTANCE_ID_ENDPOINT, payload: 'instance_id.json' }], + [], + done, + ); + }); + }); + + describe('setInstanceId', () => { + it('should commit SET_INSTANCE_ID mutation', done => { + testAction( + setInstanceId, + 'test_instance_id', + mockedState, + [{ type: types.SET_INSTANCE_ID, payload: 'test_instance_id' }], + [], + done, + ); + }); + }); + + describe('fetchFeatureFlags', () => { + let mock; + + beforeEach(() => { + mockedState.endpoint = `${TEST_HOST}/endpoint.json`; + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('success', () => { + it('dispatches requestFeatureFlags and receiveFeatureFlagsSuccess ', done => { + mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, getRequestData, {}); + + testAction( + fetchFeatureFlags, + null, + mockedState, + [], + [ + { + type: 'requestFeatureFlags', + }, + { + payload: { data: getRequestData, headers: {} }, + type: 'receiveFeatureFlagsSuccess', + }, + ], + done, + ); + }); + }); + + describe('error', () => { + it('dispatches requestFeatureFlags and receiveFeatureFlagsError ', done => { + mock.onGet(`${TEST_HOST}/endpoint.json`, {}).replyOnce(500, {}); + + testAction( + fetchFeatureFlags, + null, + mockedState, + [], + [ + { + type: 'requestFeatureFlags', + }, + { + type: 'receiveFeatureFlagsError', + }, + ], + done, + ); + }); + }); + }); + + describe('requestFeatureFlags', () => { + it('should commit RECEIVE_FEATURE_FLAGS_SUCCESS mutation', done => { + testAction( + requestFeatureFlags, + null, + mockedState, + [{ type: types.REQUEST_FEATURE_FLAGS }], + [], + done, + ); + }); + }); + + describe('receiveFeatureFlagsSuccess', () => { + it('should commit RECEIVE_FEATURE_FLAGS_SUCCESS mutation', done => { + testAction( + receiveFeatureFlagsSuccess, + { data: getRequestData, headers: {} }, + mockedState, + [ + { + type: types.RECEIVE_FEATURE_FLAGS_SUCCESS, + payload: { data: getRequestData, headers: {} }, + }, + ], + [], + done, + ); + }); + }); + + describe('receiveFeatureFlagsError', () => { + it('should commit RECEIVE_FEATURE_FLAGS_ERROR mutation', done => { + testAction( + receiveFeatureFlagsError, + null, + mockedState, + [{ type: types.RECEIVE_FEATURE_FLAGS_ERROR }], + [], + done, + ); + }); + }); + + describe('fetchUserLists', () => { + beforeEach(() => { + Api.fetchFeatureFlagUserLists.mockResolvedValue({ data: [userList], headers: {} }); + }); + + describe('success', () => { + it('dispatches requestUserLists and receiveUserListsSuccess ', done => { + testAction( + fetchUserLists, + null, + mockedState, + [], + [ + { + type: 'requestUserLists', + }, + { + payload: { data: [userList], headers: {} }, + type: 'receiveUserListsSuccess', + }, + ], + done, + ); + }); + }); + + describe('error', () => { + it('dispatches requestUserLists and receiveUserListsError ', done => { + Api.fetchFeatureFlagUserLists.mockRejectedValue(); + + testAction( + fetchUserLists, + null, + mockedState, + [], + [ + { + type: 'requestUserLists', + }, + { + type: 'receiveUserListsError', + }, + ], + done, + ); + }); + }); + }); + + describe('requestUserLists', () => { + it('should commit RECEIVE_USER_LISTS_SUCCESS mutation', done => { + testAction( + requestUserLists, + null, + mockedState, + [{ type: types.REQUEST_USER_LISTS }], + [], + done, + ); + }); + }); + + describe('receiveUserListsSuccess', () => { + it('should commit RECEIVE_USER_LISTS_SUCCESS mutation', done => { + testAction( + receiveUserListsSuccess, + { data: [userList], headers: {} }, + mockedState, + [ + { + type: types.RECEIVE_USER_LISTS_SUCCESS, + payload: { data: [userList], headers: {} }, + }, + ], + [], + done, + ); + }); + }); + + describe('receiveUserListsError', () => { + it('should commit RECEIVE_USER_LISTS_ERROR mutation', done => { + testAction( + receiveUserListsError, + null, + mockedState, + [{ type: types.RECEIVE_USER_LISTS_ERROR }], + [], + done, + ); + }); + }); + + describe('rotateInstanceId', () => { + let mock; + + beforeEach(() => { + mockedState.rotateEndpoint = `${TEST_HOST}/endpoint.json`; + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('success', () => { + it('dispatches requestRotateInstanceId and receiveRotateInstanceIdSuccess ', done => { + mock.onPost(`${TEST_HOST}/endpoint.json`).replyOnce(200, rotateData, {}); + + testAction( + rotateInstanceId, + null, + mockedState, + [], + [ + { + type: 'requestRotateInstanceId', + }, + { + payload: { data: rotateData, headers: {} }, + type: 'receiveRotateInstanceIdSuccess', + }, + ], + done, + ); + }); + }); + + describe('error', () => { + it('dispatches requestRotateInstanceId and receiveRotateInstanceIdError ', done => { + mock.onGet(`${TEST_HOST}/endpoint.json`, {}).replyOnce(500, {}); + + testAction( + rotateInstanceId, + null, + mockedState, + [], + [ + { + type: 'requestRotateInstanceId', + }, + { + type: 'receiveRotateInstanceIdError', + }, + ], + done, + ); + }); + }); + }); + + describe('requestRotateInstanceId', () => { + it('should commit REQUEST_ROTATE_INSTANCE_ID mutation', done => { + testAction( + requestRotateInstanceId, + null, + mockedState, + [{ type: types.REQUEST_ROTATE_INSTANCE_ID }], + [], + done, + ); + }); + }); + + describe('receiveRotateInstanceIdSuccess', () => { + it('should commit RECEIVE_ROTATE_INSTANCE_ID_SUCCESS mutation', done => { + testAction( + receiveRotateInstanceIdSuccess, + { data: rotateData, headers: {} }, + mockedState, + [ + { + type: types.RECEIVE_ROTATE_INSTANCE_ID_SUCCESS, + payload: { data: rotateData, headers: {} }, + }, + ], + [], + done, + ); + }); + }); + + describe('receiveRotateInstanceIdError', () => { + it('should commit RECEIVE_ROTATE_INSTANCE_ID_ERROR mutation', done => { + testAction( + receiveRotateInstanceIdError, + null, + mockedState, + [{ type: types.RECEIVE_ROTATE_INSTANCE_ID_ERROR }], + [], + done, + ); + }); + }); + + describe('toggleFeatureFlag', () => { + let mock; + + beforeEach(() => { + mockedState.featureFlags = getRequestData.feature_flags.map(flag => ({ + ...flag, + scopes: mapToScopesViewModel(flag.scopes || []), + })); + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + describe('success', () => { + it('dispatches updateFeatureFlag and receiveUpdateFeatureFlagSuccess', done => { + mock.onPut(featureFlag.update_path).replyOnce(200, featureFlag, {}); + + testAction( + toggleFeatureFlag, + featureFlag, + mockedState, + [], + [ + { + type: 'updateFeatureFlag', + payload: featureFlag, + }, + { + payload: featureFlag, + type: 'receiveUpdateFeatureFlagSuccess', + }, + ], + done, + ); + }); + }); + describe('error', () => { + it('dispatches updateFeatureFlag and receiveUpdateFeatureFlagSuccess', done => { + mock.onPut(featureFlag.update_path).replyOnce(500); + + testAction( + toggleFeatureFlag, + featureFlag, + mockedState, + [], + [ + { + type: 'updateFeatureFlag', + payload: featureFlag, + }, + { + payload: featureFlag.id, + type: 'receiveUpdateFeatureFlagError', + }, + ], + done, + ); + }); + }); + }); + describe('updateFeatureFlag', () => { + beforeEach(() => { + mockedState.featureFlags = getRequestData.feature_flags.map(f => ({ + ...f, + scopes: mapToScopesViewModel(f.scopes || []), + })); + }); + + it('commits UPDATE_FEATURE_FLAG with the given flag', done => { + testAction( + updateFeatureFlag, + featureFlag, + mockedState, + [ + { + type: 'UPDATE_FEATURE_FLAG', + payload: featureFlag, + }, + ], + [], + done, + ); + }); + }); + describe('receiveUpdateFeatureFlagSuccess', () => { + beforeEach(() => { + mockedState.featureFlags = getRequestData.feature_flags.map(f => ({ + ...f, + scopes: mapToScopesViewModel(f.scopes || []), + })); + }); + + it('commits RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS with the given flag', done => { + testAction( + receiveUpdateFeatureFlagSuccess, + featureFlag, + mockedState, + [ + { + type: 'RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS', + payload: featureFlag, + }, + ], + [], + done, + ); + }); + }); + describe('receiveUpdateFeatureFlagError', () => { + beforeEach(() => { + mockedState.featureFlags = getRequestData.feature_flags.map(f => ({ + ...f, + scopes: mapToScopesViewModel(f.scopes || []), + })); + }); + + it('commits RECEIVE_UPDATE_FEATURE_FLAG_ERROR with the given flag id', done => { + testAction( + receiveUpdateFeatureFlagError, + featureFlag.id, + mockedState, + [ + { + type: 'RECEIVE_UPDATE_FEATURE_FLAG_ERROR', + payload: featureFlag.id, + }, + ], + [], + done, + ); + }); + }); + describe('deleteUserList', () => { + beforeEach(() => { + mockedState.userLists = [userList]; + }); + + describe('success', () => { + beforeEach(() => { + Api.deleteFeatureFlagUserList.mockResolvedValue(); + }); + + it('should refresh the user lists', done => { + testAction( + deleteUserList, + userList, + mockedState, + [], + [{ type: 'requestDeleteUserList', payload: userList }, { type: 'fetchUserLists' }], + done, + ); + }); + }); + + describe('error', () => { + beforeEach(() => { + Api.deleteFeatureFlagUserList.mockRejectedValue({ response: { data: 'some error' } }); + }); + + it('should dispatch receiveDeleteUserListError', done => { + testAction( + deleteUserList, + userList, + mockedState, + [], + [ + { type: 'requestDeleteUserList', payload: userList }, + { + type: 'receiveDeleteUserListError', + payload: { list: userList, error: 'some error' }, + }, + ], + done, + ); + }); + }); + }); + + describe('receiveDeleteUserListError', () => { + it('should commit RECEIVE_DELETE_USER_LIST_ERROR with the given list', done => { + testAction( + receiveDeleteUserListError, + { list: userList, error: 'mock error' }, + mockedState, + [ + { + type: 'RECEIVE_DELETE_USER_LIST_ERROR', + payload: { list: userList, error: 'mock error' }, + }, + ], + [], + done, + ); + }); + }); + + describe('clearAlert', () => { + it('should commit RECEIVE_CLEAR_ALERT', done => { + const alertIndex = 3; + + testAction( + clearAlert, + alertIndex, + mockedState, + [{ type: 'RECEIVE_CLEAR_ALERT', payload: alertIndex }], + [], + done, + ); + }); + }); +}); diff --git a/spec/frontend/feature_flags/store/index/mutations_spec.js b/spec/frontend/feature_flags/store/index/mutations_spec.js new file mode 100644 index 00000000000..5e236fe2222 --- /dev/null +++ b/spec/frontend/feature_flags/store/index/mutations_spec.js @@ -0,0 +1,332 @@ +import state from '~/feature_flags/store/modules/index/state'; +import mutations from '~/feature_flags/store/modules/index/mutations'; +import * as types from '~/feature_flags/store/modules/index/mutation_types'; +import { mapToScopesViewModel } from '~/feature_flags/store/modules/helpers'; +import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; +import { getRequestData, rotateData, featureFlag, userList } from '../../mock_data'; + +describe('Feature flags store Mutations', () => { + let stateCopy; + + beforeEach(() => { + stateCopy = state(); + }); + + describe('SET_FEATURE_FLAGS_ENDPOINT', () => { + it('should set endpoint', () => { + mutations[types.SET_FEATURE_FLAGS_ENDPOINT](stateCopy, 'feature_flags.json'); + + expect(stateCopy.endpoint).toEqual('feature_flags.json'); + }); + }); + + describe('SET_FEATURE_FLAGS_OPTIONS', () => { + it('should set provided options', () => { + mutations[types.SET_FEATURE_FLAGS_OPTIONS](stateCopy, { page: '1', scope: 'all' }); + + expect(stateCopy.options).toEqual({ page: '1', scope: 'all' }); + }); + }); + + describe('SET_INSTANCE_ID_ENDPOINT', () => { + it('should set provided endpoint', () => { + mutations[types.SET_INSTANCE_ID_ENDPOINT](stateCopy, 'rotate_token.json'); + + expect(stateCopy.rotateEndpoint).toEqual('rotate_token.json'); + }); + }); + + describe('SET_INSTANCE_ID', () => { + it('should set provided token', () => { + mutations[types.SET_INSTANCE_ID](stateCopy, rotateData.token); + + expect(stateCopy.instanceId).toEqual(rotateData.token); + }); + }); + + describe('REQUEST_FEATURE_FLAGS', () => { + it('should set isLoading to true', () => { + mutations[types.REQUEST_FEATURE_FLAGS](stateCopy); + + expect(stateCopy.isLoading).toEqual(true); + }); + }); + + describe('RECEIVE_FEATURE_FLAGS_SUCCESS', () => { + const headers = { + 'x-next-page': '2', + 'x-page': '1', + 'X-Per-Page': '2', + 'X-Prev-Page': '', + 'X-TOTAL': '37', + 'X-Total-Pages': '5', + }; + + beforeEach(() => { + mutations[types.RECEIVE_FEATURE_FLAGS_SUCCESS](stateCopy, { data: getRequestData, headers }); + }); + + it('should set isLoading to false', () => { + expect(stateCopy.isLoading).toEqual(false); + }); + + it('should set hasError to false', () => { + expect(stateCopy.hasError).toEqual(false); + }); + + it('should set featureFlags with the transformed data', () => { + const expected = getRequestData.feature_flags.map(flag => ({ + ...flag, + scopes: mapToScopesViewModel(flag.scopes || []), + })); + + expect(stateCopy.featureFlags).toEqual(expected); + }); + + it('should set count with the given data', () => { + expect(stateCopy.count.featureFlags).toEqual(37); + }); + + it('should set pagination', () => { + expect(stateCopy.pageInfo.featureFlags).toEqual( + parseIntPagination(normalizeHeaders(headers)), + ); + }); + }); + + describe('RECEIVE_FEATURE_FLAGS_ERROR', () => { + beforeEach(() => { + mutations[types.RECEIVE_FEATURE_FLAGS_ERROR](stateCopy); + }); + + it('should set isLoading to false', () => { + expect(stateCopy.isLoading).toEqual(false); + }); + + it('should set hasError to true', () => { + expect(stateCopy.hasError).toEqual(true); + }); + }); + + describe('REQUEST_USER_LISTS', () => { + it('sets isLoading to true', () => { + mutations[types.REQUEST_USER_LISTS](stateCopy); + expect(stateCopy.isLoading).toBe(true); + }); + }); + + describe('RECEIVE_USER_LISTS_SUCCESS', () => { + const headers = { + 'x-next-page': '2', + 'x-page': '1', + 'X-Per-Page': '2', + 'X-Prev-Page': '', + 'X-TOTAL': '37', + 'X-Total-Pages': '5', + }; + + beforeEach(() => { + mutations[types.RECEIVE_USER_LISTS_SUCCESS](stateCopy, { data: [userList], headers }); + }); + + it('sets isLoading to false', () => { + expect(stateCopy.isLoading).toBe(false); + }); + + it('sets userLists to the received userLists', () => { + expect(stateCopy.userLists).toEqual([userList]); + }); + + it('sets pagination info for user lits', () => { + expect(stateCopy.pageInfo.userLists).toEqual(parseIntPagination(normalizeHeaders(headers))); + }); + + it('sets the count for user lists', () => { + expect(stateCopy.count.userLists).toBe(parseInt(headers['X-TOTAL'], 10)); + }); + }); + + describe('RECEIVE_USER_LISTS_ERROR', () => { + beforeEach(() => { + mutations[types.RECEIVE_USER_LISTS_ERROR](stateCopy); + }); + + it('should set isLoading to false', () => { + expect(stateCopy.isLoading).toEqual(false); + }); + + it('should set hasError to true', () => { + expect(stateCopy.hasError).toEqual(true); + }); + }); + + describe('REQUEST_ROTATE_INSTANCE_ID', () => { + beforeEach(() => { + mutations[types.REQUEST_ROTATE_INSTANCE_ID](stateCopy); + }); + + it('should set isRotating to true', () => { + expect(stateCopy.isRotating).toBe(true); + }); + + it('should set hasRotateError to false', () => { + expect(stateCopy.hasRotateError).toBe(false); + }); + }); + + describe('RECEIVE_ROTATE_INSTANCE_ID_SUCCESS', () => { + beforeEach(() => { + mutations[types.RECEIVE_ROTATE_INSTANCE_ID_SUCCESS](stateCopy, { data: rotateData }); + }); + + it('should set the instance id to the received data', () => { + expect(stateCopy.instanceId).toBe(rotateData.token); + }); + + it('should set isRotating to false', () => { + expect(stateCopy.isRotating).toBe(false); + }); + + it('should set hasRotateError to false', () => { + expect(stateCopy.hasRotateError).toBe(false); + }); + }); + + describe('RECEIVE_ROTATE_INSTANCE_ID_ERROR', () => { + beforeEach(() => { + mutations[types.RECEIVE_ROTATE_INSTANCE_ID_ERROR](stateCopy); + }); + + it('should set isRotating to false', () => { + expect(stateCopy.isRotating).toBe(false); + }); + + it('should set hasRotateError to true', () => { + expect(stateCopy.hasRotateError).toBe(true); + }); + }); + + describe('UPDATE_FEATURE_FLAG', () => { + beforeEach(() => { + stateCopy.featureFlags = getRequestData.feature_flags.map(flag => ({ + ...flag, + scopes: mapToScopesViewModel(flag.scopes || []), + })); + stateCopy.count = { featureFlags: 1, userLists: 0 }; + + mutations[types.UPDATE_FEATURE_FLAG](stateCopy, { + ...featureFlag, + scopes: mapToScopesViewModel(featureFlag.scopes || []), + active: false, + }); + }); + + it('should update the flag with the matching ID', () => { + expect(stateCopy.featureFlags).toEqual([ + { + ...featureFlag, + scopes: mapToScopesViewModel(featureFlag.scopes || []), + active: false, + }, + ]); + }); + }); + + describe('RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS', () => { + const runUpdate = (stateCount, flagState, featureFlagUpdateParams) => { + stateCopy.featureFlags = getRequestData.feature_flags.map(flag => ({ + ...flag, + ...flagState, + scopes: mapToScopesViewModel(flag.scopes || []), + })); + stateCopy.count.featureFlags = stateCount; + + mutations[types.RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS](stateCopy, { + ...featureFlag, + ...featureFlagUpdateParams, + }); + }; + + it('updates the flag with the matching ID', () => { + runUpdate({ all: 1, enabled: 1, disabled: 0 }, { active: true }, { active: false }); + + expect(stateCopy.featureFlags).toEqual([ + { + ...featureFlag, + scopes: mapToScopesViewModel(featureFlag.scopes || []), + active: false, + }, + ]); + }); + }); + + describe('RECEIVE_UPDATE_FEATURE_FLAG_ERROR', () => { + beforeEach(() => { + stateCopy.featureFlags = getRequestData.feature_flags.map(flag => ({ + ...flag, + scopes: mapToScopesViewModel(flag.scopes || []), + })); + stateCopy.count = { enabled: 1, disabled: 0 }; + + mutations[types.RECEIVE_UPDATE_FEATURE_FLAG_ERROR](stateCopy, featureFlag.id); + }); + + it('should update the flag with the matching ID, toggling active', () => { + expect(stateCopy.featureFlags).toEqual([ + { + ...featureFlag, + scopes: mapToScopesViewModel(featureFlag.scopes || []), + active: false, + }, + ]); + }); + }); + + describe('REQUEST_DELETE_USER_LIST', () => { + beforeEach(() => { + stateCopy.userLists = [userList]; + mutations[types.REQUEST_DELETE_USER_LIST](stateCopy, userList); + }); + + it('should remove the deleted list', () => { + expect(stateCopy.userLists).not.toContain(userList); + }); + }); + + describe('RECEIVE_DELETE_USER_LIST_ERROR', () => { + beforeEach(() => { + stateCopy.userLists = []; + mutations[types.RECEIVE_DELETE_USER_LIST_ERROR](stateCopy, { + list: userList, + error: 'some error', + }); + }); + + it('should set isLoading to false and hasError to false', () => { + expect(stateCopy.isLoading).toBe(false); + expect(stateCopy.hasError).toBe(false); + }); + + it('should add the user list back to the list of user lists', () => { + expect(stateCopy.userLists).toContain(userList); + }); + }); + + describe('RECEIVE_CLEAR_ALERT', () => { + it('clears the alert', () => { + stateCopy.alerts = ['a server error']; + + mutations[types.RECEIVE_CLEAR_ALERT](stateCopy, 0); + + expect(stateCopy.alerts).toEqual([]); + }); + + it('clears the alert at the specified index', () => { + stateCopy.alerts = ['a server error', 'another error', 'final error']; + + mutations[types.RECEIVE_CLEAR_ALERT](stateCopy, 1); + + expect(stateCopy.alerts).toEqual(['a server error', 'final error']); + }); + }); +}); diff --git a/spec/frontend/feature_flags/store/new/actions_spec.js b/spec/frontend/feature_flags/store/new/actions_spec.js new file mode 100644 index 00000000000..cfcddd9451f --- /dev/null +++ b/spec/frontend/feature_flags/store/new/actions_spec.js @@ -0,0 +1,223 @@ +import MockAdapter from 'axios-mock-adapter'; +import testAction from 'helpers/vuex_action_helper'; +import { TEST_HOST } from 'spec/test_constants'; +import { + setEndpoint, + setPath, + createFeatureFlag, + requestCreateFeatureFlag, + receiveCreateFeatureFlagSuccess, + receiveCreateFeatureFlagError, +} from '~/feature_flags/store/modules/new/actions'; +import state from '~/feature_flags/store/modules/new/state'; +import * as types from '~/feature_flags/store/modules/new/mutation_types'; +import { + ROLLOUT_STRATEGY_ALL_USERS, + ROLLOUT_STRATEGY_PERCENT_ROLLOUT, + LEGACY_FLAG, + NEW_VERSION_FLAG, +} from '~/feature_flags/constants'; +import { + mapFromScopesViewModel, + mapStrategiesToRails, +} from '~/feature_flags/store/modules/helpers'; +import axios from '~/lib/utils/axios_utils'; + +jest.mock('~/lib/utils/url_utility'); + +describe('Feature flags New Module Actions', () => { + let mockedState; + + beforeEach(() => { + mockedState = state(); + }); + + describe('setEndpoint', () => { + it('should commit SET_ENDPOINT mutation', done => { + testAction( + setEndpoint, + 'feature_flags.json', + mockedState, + [{ type: types.SET_ENDPOINT, payload: 'feature_flags.json' }], + [], + done, + ); + }); + }); + + describe('setPath', () => { + it('should commit SET_PATH mutation', done => { + testAction( + setPath, + '/feature_flags', + mockedState, + [{ type: types.SET_PATH, payload: '/feature_flags' }], + [], + done, + ); + }); + }); + + describe('createFeatureFlag', () => { + let mock; + + const actionParams = { + name: 'name', + description: 'description', + active: true, + version: LEGACY_FLAG, + scopes: [ + { + id: 1, + environmentScope: 'environmentScope', + active: true, + canUpdate: true, + protected: true, + shouldBeDestroyed: false, + rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS, + rolloutPercentage: ROLLOUT_STRATEGY_PERCENT_ROLLOUT, + }, + ], + }; + + beforeEach(() => { + mockedState.endpoint = `${TEST_HOST}/endpoint.json`; + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('success', () => { + it('dispatches requestCreateFeatureFlag and receiveCreateFeatureFlagSuccess ', done => { + const convertedActionParams = mapFromScopesViewModel(actionParams); + + mock.onPost(`${TEST_HOST}/endpoint.json`, convertedActionParams).replyOnce(200); + + testAction( + createFeatureFlag, + actionParams, + mockedState, + [], + [ + { + type: 'requestCreateFeatureFlag', + }, + { + type: 'receiveCreateFeatureFlagSuccess', + }, + ], + done, + ); + }); + + it('sends strategies for new style feature flags', done => { + const newVersionFlagParams = { + name: 'name', + description: 'description', + active: true, + version: NEW_VERSION_FLAG, + strategies: [ + { + name: ROLLOUT_STRATEGY_ALL_USERS, + parameters: {}, + id: 1, + scopes: [{ id: 1, environmentScope: 'environmentScope', shouldBeDestroyed: false }], + shouldBeDestroyed: false, + }, + ], + }; + mock + .onPost(`${TEST_HOST}/endpoint.json`, mapStrategiesToRails(newVersionFlagParams)) + .replyOnce(200); + + testAction( + createFeatureFlag, + newVersionFlagParams, + mockedState, + [], + [ + { + type: 'requestCreateFeatureFlag', + }, + { + type: 'receiveCreateFeatureFlagSuccess', + }, + ], + done, + ); + }); + }); + + describe('error', () => { + it('dispatches requestCreateFeatureFlag and receiveCreateFeatureFlagError ', done => { + const convertedActionParams = mapFromScopesViewModel(actionParams); + + mock + .onPost(`${TEST_HOST}/endpoint.json`, convertedActionParams) + .replyOnce(500, { message: [] }); + + testAction( + createFeatureFlag, + actionParams, + mockedState, + [], + [ + { + type: 'requestCreateFeatureFlag', + }, + { + type: 'receiveCreateFeatureFlagError', + payload: { message: [] }, + }, + ], + done, + ); + }); + }); + }); + + describe('requestCreateFeatureFlag', () => { + it('should commit REQUEST_CREATE_FEATURE_FLAG mutation', done => { + testAction( + requestCreateFeatureFlag, + null, + mockedState, + [{ type: types.REQUEST_CREATE_FEATURE_FLAG }], + [], + done, + ); + }); + }); + + describe('receiveCreateFeatureFlagSuccess', () => { + it('should commit RECEIVE_CREATE_FEATURE_FLAG_SUCCESS mutation', done => { + testAction( + receiveCreateFeatureFlagSuccess, + null, + mockedState, + [ + { + type: types.RECEIVE_CREATE_FEATURE_FLAG_SUCCESS, + }, + ], + [], + done, + ); + }); + }); + + describe('receiveCreateFeatureFlagError', () => { + it('should commit RECEIVE_CREATE_FEATURE_FLAG_ERROR mutation', done => { + testAction( + receiveCreateFeatureFlagError, + 'There was an error', + mockedState, + [{ type: types.RECEIVE_CREATE_FEATURE_FLAG_ERROR, payload: 'There was an error' }], + [], + done, + ); + }); + }); +}); diff --git a/spec/frontend/feature_flags/store/new/mutations_spec.js b/spec/frontend/feature_flags/store/new/mutations_spec.js new file mode 100644 index 00000000000..95eba96ed72 --- /dev/null +++ b/spec/frontend/feature_flags/store/new/mutations_spec.js @@ -0,0 +1,65 @@ +import state from '~/feature_flags/store/modules/new/state'; +import mutations from '~/feature_flags/store/modules/new/mutations'; +import * as types from '~/feature_flags/store/modules/new/mutation_types'; + +describe('Feature flags New Module Mutations', () => { + let stateCopy; + + beforeEach(() => { + stateCopy = state(); + }); + + describe('SET_ENDPOINT', () => { + it('should set endpoint', () => { + mutations[types.SET_ENDPOINT](stateCopy, 'feature_flags.json'); + + expect(stateCopy.endpoint).toEqual('feature_flags.json'); + }); + }); + + describe('SET_PATH', () => { + it('should set provided options', () => { + mutations[types.SET_PATH](stateCopy, 'feature_flags'); + + expect(stateCopy.path).toEqual('feature_flags'); + }); + }); + + describe('REQUEST_CREATE_FEATURE_FLAG', () => { + it('should set isSendingRequest to true', () => { + mutations[types.REQUEST_CREATE_FEATURE_FLAG](stateCopy); + + expect(stateCopy.isSendingRequest).toEqual(true); + }); + + it('should set error to an empty array', () => { + mutations[types.REQUEST_CREATE_FEATURE_FLAG](stateCopy); + + expect(stateCopy.error).toEqual([]); + }); + }); + + describe('RECEIVE_CREATE_FEATURE_FLAG_SUCCESS', () => { + it('should set isSendingRequest to false', () => { + mutations[types.RECEIVE_CREATE_FEATURE_FLAG_SUCCESS](stateCopy); + + expect(stateCopy.isSendingRequest).toEqual(false); + }); + }); + + describe('RECEIVE_CREATE_FEATURE_FLAG_ERROR', () => { + beforeEach(() => { + mutations[types.RECEIVE_CREATE_FEATURE_FLAG_ERROR](stateCopy, { + message: ['Name is required'], + }); + }); + + it('should set isSendingRequest to false', () => { + expect(stateCopy.isSendingRequest).toEqual(false); + }); + + it('should set hasError to true', () => { + expect(stateCopy.error).toEqual(['Name is required']); + }); + }); +}); |