diff --git a/ui/app/helpers/wizard-constants.js b/ui/app/helpers/wizard-constants.js new file mode 100644 index 0000000000..f3b1e13b23 --- /dev/null +++ b/ui/app/helpers/wizard-constants.js @@ -0,0 +1,39 @@ +import TutorialMachineConfig from 'vault/machines/tutorial-machine'; +import SecretsMachineConfig from 'vault/machines/secrets-machine'; +import PoliciesMachineConfig from 'vault/machines/policies-machine'; +import ReplicationMachineConfig from 'vault/machines/replication-machine'; +import ToolsMachineConfig from 'vault/machines/tools-machine'; +import AuthMachineConfig from 'vault/machines/auth-machine'; + +export const STORAGE_KEYS = { + TUTORIAL_STATE: 'vault:ui-tutorial-state', + FEATURE_LIST: 'vault:ui-feature-list', + FEATURE_STATE: 'vault:ui-feature-state', + COMPLETED_FEATURES: 'vault:ui-completed-list', + COMPONENT_STATE: 'vault:ui-component-state', + RESUME_URL: 'vault:ui-tutorial-resume-url', + RESUME_ROUTE: 'vault:ui-tutorial-resume-route', +}; + +export const MACHINES = { + tutorial: TutorialMachineConfig, + secrets: SecretsMachineConfig, + policies: PoliciesMachineConfig, + replication: ReplicationMachineConfig, + tools: ToolsMachineConfig, + authentication: AuthMachineConfig, +}; + +export const DEFAULTS = { + currentState: null, + featureList: null, + featureState: null, + currentMachine: null, + tutorialComponent: null, + featureComponent: null, + stepComponent: null, + detailsComponent: null, + componentState: null, + nextFeature: null, + nextStep: null, +}; diff --git a/ui/app/machines/replication-machine.js b/ui/app/machines/replication-machine.js index 2a0efc1d77..99bf8b2735 100644 --- a/ui/app/machines/replication-machine.js +++ b/ui/app/machines/replication-machine.js @@ -19,7 +19,6 @@ export default { }, complete: { onEntry: ['completeFeature'], - on: { RESET: 'idle' }, }, }, }; diff --git a/ui/app/machines/secrets-machine.js b/ui/app/machines/secrets-machine.js index 8ac5eba70c..1a75282b5f 100644 --- a/ui/app/machines/secrets-machine.js +++ b/ui/app/machines/secrets-machine.js @@ -43,7 +43,7 @@ export default { cond: type => ['pki', 'aws', 'ssh'].includes(type), }, secret: { - cond: type => ['cubbyhole', 'database', 'gcp', 'kv', 'nomad', 'rabbitmq', 'totp'].includes(type), + cond: type => ['kv'].includes(type), }, encryption: { cond: type => type === 'transit', @@ -108,7 +108,7 @@ export default { actions: [{ type: 'routeTransition', params: ['vault.cluster.secrets.backend.create-root'] }], }, secret: { - cond: type => ['cubbyhole', 'database', 'gcp', 'kv', 'nomad', 'rabbitmq', 'totp'].includes(type), + cond: type => ['kv'].includes(type), actions: [{ type: 'routeTransition', params: ['vault.cluster.secrets.backend.create-root'] }], }, encryption: { diff --git a/ui/app/machines/tools-machine.js b/ui/app/machines/tools-machine.js index bde4890698..67b2734eca 100644 --- a/ui/app/machines/tools-machine.js +++ b/ui/app/machines/tools-machine.js @@ -55,7 +55,6 @@ export default { }, complete: { onEntry: ['completeFeature'], - on: { RESET: 'idle' }, }, }, }; diff --git a/ui/app/services/wizard.js b/ui/app/services/wizard.js index 81735217b1..95e1db7563 100644 --- a/ui/app/services/wizard.js +++ b/ui/app/services/wizard.js @@ -4,44 +4,9 @@ import Service, { inject as service } from '@ember/service'; import { Machine } from 'xstate'; import getStorage from 'vault/lib/token-storage'; - -import TutorialMachineConfig from 'vault/machines/tutorial-machine'; -import SecretsMachineConfig from 'vault/machines/secrets-machine'; -import PoliciesMachineConfig from 'vault/machines/policies-machine'; -import ReplicationMachineConfig from 'vault/machines/replication-machine'; -import ToolsMachineConfig from 'vault/machines/tools-machine'; -import AuthMachineConfig from 'vault/machines/auth-machine'; - -const TutorialMachine = Machine(TutorialMachineConfig); +import { STORAGE_KEYS, DEFAULTS, MACHINES } from 'vault/helpers/wizard-constants'; +const TutorialMachine = Machine(MACHINES.tutorial); let FeatureMachine = null; -const TUTORIAL_STATE = 'vault:ui-tutorial-state'; -const FEATURE_LIST = 'vault:ui-feature-list'; -const FEATURE_STATE = 'vault:ui-feature-state'; -const COMPLETED_FEATURES = 'vault:ui-completed-list'; -const COMPONENT_STATE = 'vault:ui-component-state'; -const RESUME_URL = 'vault:ui-tutorial-resume-url'; -const RESUME_ROUTE = 'vault:ui-tutorial-resume-route'; -const MACHINES = { - secrets: SecretsMachineConfig, - policies: PoliciesMachineConfig, - replication: ReplicationMachineConfig, - tools: ToolsMachineConfig, - authentication: AuthMachineConfig, -}; - -const DEFAULTS = { - currentState: null, - featureList: null, - featureState: null, - currentMachine: null, - tutorialComponent: null, - featureComponent: null, - stepComponent: null, - detailsComponent: null, - componentState: null, - nextFeature: null, - nextStep: null, -}; export default Service.extend(DEFAULTS, { router: service(), @@ -53,25 +18,25 @@ export default Service.extend(DEFAULTS, { }, initializeMachines() { - if (!this.storageHasKey(TUTORIAL_STATE)) { + if (!this.storageHasKey(STORAGE_KEYS.TUTORIAL_STATE)) { let state = TutorialMachine.initialState; this.saveState('currentState', state.value); - this.saveExtState(TUTORIAL_STATE, state.value); + this.saveExtState(STORAGE_KEYS.TUTORIAL_STATE, state.value); } - this.saveState('currentState', this.getExtState(TUTORIAL_STATE)); - if (this.storageHasKey(COMPONENT_STATE)) { - this.set('componentState', this.getExtState(COMPONENT_STATE)); + this.saveState('currentState', this.getExtState(STORAGE_KEYS.TUTORIAL_STATE)); + if (this.storageHasKey(STORAGE_KEYS.COMPONENT_STATE)) { + this.set('componentState', this.getExtState(STORAGE_KEYS.COMPONENT_STATE)); } let stateNodes = TutorialMachine.getStateNodes(this.get('currentState')); this.executeActions(stateNodes.reduce((acc, node) => acc.concat(node.onEntry), []), null, 'tutorial'); - if (this.storageHasKey(FEATURE_LIST)) { - this.set('featureList', this.getExtState(FEATURE_LIST)); - if (this.storageHasKey(FEATURE_STATE)) { - this.saveState('featureState', this.getExtState(FEATURE_STATE)); + if (this.storageHasKey(STORAGE_KEYS.FEATURE_LIST)) { + this.set('featureList', this.getExtState(STORAGE_KEYS.FEATURE_LIST)); + if (this.storageHasKey(STORAGE_KEYS.FEATURE_STATE)) { + this.saveState('featureState', this.getExtState(STORAGE_KEYS.FEATURE_STATE)); } else { if (FeatureMachine != null) { this.saveState('featureState', FeatureMachine.initialState); - this.saveExtState(FEATURE_STATE, this.get('featureState')); + this.saveExtState(STORAGE_KEYS.FEATURE_STATE, this.get('featureState')); } } this.buildFeatureMachine(); @@ -82,13 +47,13 @@ export default Service.extend(DEFAULTS, { let storage = this.storage(); // empty storage [ - TUTORIAL_STATE, - FEATURE_LIST, - FEATURE_STATE, - COMPLETED_FEATURES, - COMPONENT_STATE, - RESUME_URL, - RESUME_ROUTE, + STORAGE_KEYS.TUTORIAL_STATE, + STORAGE_KEYS.FEATURE_LIST, + STORAGE_KEYS.FEATURE_STATE, + STORAGE_KEYS.COMPLETED_FEATURES, + STORAGE_KEYS.COMPONENT_STATE, + STORAGE_KEYS.RESUME_URL, + STORAGE_KEYS.RESUME_ROUTE, ].forEach(key => storage.removeItem(key)); // reset wizard state this.setProperties(DEFAULTS); @@ -115,11 +80,11 @@ export default Service.extend(DEFAULTS, { transitionTutorialMachine(currentState, event, extendedState) { if (extendedState) { this.set('componentState', extendedState); - this.saveExtState(COMPONENT_STATE, extendedState); + this.saveExtState(STORAGE_KEYS.COMPONENT_STATE, extendedState); } let { actions, value } = TutorialMachine.transition(currentState, event); this.saveState('currentState', value); - this.saveExtState(TUTORIAL_STATE, this.get('currentState')); + this.saveExtState(STORAGE_KEYS.TUTORIAL_STATE, this.get('currentState')); this.executeActions(actions, event, 'tutorial'); }, @@ -129,12 +94,12 @@ export default Service.extend(DEFAULTS, { } if (extendedState) { this.set('componentState', extendedState); - this.saveExtState(COMPONENT_STATE, extendedState); + this.saveExtState(STORAGE_KEYS.COMPONENT_STATE, extendedState); } let { actions, value } = FeatureMachine.transition(currentState, event, this.get('componentState')); this.saveState('featureState', value); - this.saveExtState(FEATURE_STATE, value); + this.saveExtState(STORAGE_KEYS.FEATURE_STATE, value); this.executeActions(actions, event, 'feature'); // if all features were completed, the FeatureMachine gets nulled // out and won't exist here as there is no next step @@ -229,33 +194,33 @@ export default Service.extend(DEFAULTS, { handlePaused() { let expected = this.get('expectedURL'); if (expected) { - this.saveExtState(RESUME_URL, this.get('expectedURL')); - this.saveExtState(RESUME_ROUTE, this.get('expectedRouteName')); + this.saveExtState(STORAGE_KEYS.RESUME_URL, this.get('expectedURL')); + this.saveExtState(STORAGE_KEYS.RESUME_ROUTE, this.get('expectedRouteName')); } }, handleResume() { - let resumeURL = this.storage().getItem(RESUME_URL); + let resumeURL = this.storage().getItem(STORAGE_KEYS.RESUME_URL); if (!resumeURL) { return; } this.get('router').transitionTo(resumeURL).followRedirects().then(() => { - this.set('expectedRouteName', this.storage().getItem(RESUME_ROUTE)); + this.set('expectedRouteName', this.storage().getItem(STORAGE_KEYS.RESUME_ROUTE)); this.set('expectedURL', resumeURL); this.initializeMachines(); - this.storage().removeItem(RESUME_URL); + this.storage().removeItem(STORAGE_KEYS.RESUME_URL); }); }, handleDismissed() { - this.storage().removeItem(FEATURE_STATE); - this.storage().removeItem(FEATURE_LIST); - this.storage().removeItem(COMPONENT_STATE); + this.storage().removeItem(STORAGE_KEYS.FEATURE_STATE); + this.storage().removeItem(STORAGE_KEYS.FEATURE_LIST); + this.storage().removeItem(STORAGE_KEYS.COMPONENT_STATE); }, saveFeatures(features) { this.set('featureList', features); - this.saveExtState(FEATURE_LIST, this.get('featureList')); + this.saveExtState(STORAGE_KEYS.FEATURE_LIST, this.get('featureList')); this.buildFeatureMachine(); }, @@ -264,10 +229,10 @@ export default Service.extend(DEFAULTS, { return; } this.startFeature(); - if (this.storageHasKey(FEATURE_STATE)) { - this.saveState('featureState', this.getExtState(FEATURE_STATE)); + if (this.storageHasKey(STORAGE_KEYS.FEATURE_STATE)) { + this.saveState('featureState', this.getExtState(STORAGE_KEYS.FEATURE_STATE)); } - this.saveExtState(FEATURE_STATE, this.get('featureState')); + this.saveExtState(STORAGE_KEYS.FEATURE_STATE, this.get('featureState')); let nextFeature = this.get('featureList').length > 1 ? this.get('featureList').objectAt(1).capitalize() : 'Finish'; this.set('nextFeature', nextFeature); @@ -292,20 +257,23 @@ export default Service.extend(DEFAULTS, { completeFeature() { let features = this.get('featureList'); let done = features.shift(); - if (!this.getExtState(COMPLETED_FEATURES)) { + if (!this.getExtState(STORAGE_KEYS.COMPLETED_FEATURES)) { let completed = []; completed.push(done); - this.saveExtState(COMPLETED_FEATURES, completed); + this.saveExtState(STORAGE_KEYS.COMPLETED_FEATURES, completed); } else { - this.saveExtState(COMPLETED_FEATURES, this.getExtState(COMPLETED_FEATURES).toArray().addObject(done)); + this.saveExtState( + STORAGE_KEYS.COMPLETED_FEATURES, + this.getExtState(STORAGE_KEYS.COMPLETED_FEATURES).toArray().addObject(done) + ); } - this.saveExtState(FEATURE_LIST, features.length ? features : null); - this.storage().removeItem(FEATURE_STATE); + this.saveExtState(STORAGE_KEYS.FEATURE_LIST, features.length ? features : null); + this.storage().removeItem(STORAGE_KEYS.FEATURE_STATE); if (features.length > 0) { this.buildFeatureMachine(); } else { - this.storage().removeItem(FEATURE_LIST); + this.storage().removeItem(STORAGE_KEYS.FEATURE_LIST); FeatureMachine = null; this.transitionTutorialMachine(this.get('currentState'), 'DONE'); } diff --git a/ui/tests/unit/machines/auth-machine-test.js b/ui/tests/unit/machines/auth-machine-test.js new file mode 100644 index 0000000000..90b179a031 --- /dev/null +++ b/ui/tests/unit/machines/auth-machine-test.js @@ -0,0 +1,87 @@ +import { module, test } from 'qunit'; +import { Machine } from 'xstate'; +import AuthMachineConfig from 'vault/machines/auth-machine'; + +module('Unit | Machine | auth-machine', function() { + const authMachine = Machine(AuthMachineConfig); + + const testCases = [ + { + currentState: authMachine.initialState, + event: 'CONTINUE', + params: null, + expectedResults: { + value: 'enable', + actions: [ + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + { component: 'wizard/auth-enable', level: 'step', type: 'render' }, + ], + }, + }, + { + currentState: 'enable', + event: 'CONTINUE', + params: null, + expectedResults: { + value: 'list', + actions: [ + { component: 'wizard/auth-list', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'list', + event: 'DETAILS', + expectedResults: { + value: 'details', + actions: [ + { component: 'wizard/auth-details', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'details', + event: 'CONTINUE', + expectedResults: { + value: 'complete', + actions: ['completeFeature'], + }, + }, + { + currentState: 'details', + event: 'RESET', + params: null, + expectedResults: { + value: 'idle', + actions: [ + { + params: ['vault.cluster.settings.auth.enable'], + type: 'routeTransition', + }, + { + component: 'wizard/mounts-wizard', + level: 'feature', + type: 'render', + }, + { + component: 'wizard/auth-idle', + level: 'step', + type: 'render', + }, + ], + }, + }, + ]; + + testCases.forEach(testCase => { + test(`transition: ${testCase.event} for currentState ${testCase.currentState} and componentState ${ + testCase.params + }`, function(assert) { + let result = authMachine.transition(testCase.currentState, testCase.event, testCase.params); + assert.equal(result.value, testCase.expectedResults.value); + assert.deepEqual(result.actions, testCase.expectedResults.actions); + }); + }); +}); diff --git a/ui/tests/unit/machines/policies-machine-test.js b/ui/tests/unit/machines/policies-machine-test.js new file mode 100644 index 0000000000..831c15b872 --- /dev/null +++ b/ui/tests/unit/machines/policies-machine-test.js @@ -0,0 +1,62 @@ +import { module, test } from 'qunit'; +import { Machine } from 'xstate'; +import PoliciesMachineConfig from 'vault/machines/policies-machine'; + +module('Unit | Machine | policies-machine', function() { + const policiesMachine = Machine(PoliciesMachineConfig); + + const testCases = [ + { + currentState: policiesMachine.initialState, + event: 'CONTINUE', + params: null, + expectedResults: { + value: 'create', + actions: [{ component: 'wizard/policies-create', level: 'feature', type: 'render' }], + }, + }, + { + currentState: 'create', + event: 'CONTINUE', + params: null, + expectedResults: { + value: 'details', + actions: [{ component: 'wizard/policies-details', level: 'feature', type: 'render' }], + }, + }, + { + currentState: 'details', + event: 'CONTINUE', + expectedResults: { + value: 'delete', + actions: [{ component: 'wizard/policies-delete', level: 'feature', type: 'render' }], + }, + }, + { + currentState: 'delete', + event: 'CONTINUE', + expectedResults: { + value: 'others', + actions: [{ component: 'wizard/policies-others', level: 'feature', type: 'render' }], + }, + }, + { + currentState: 'others', + event: 'CONTINUE', + expectedResults: { + value: 'complete', + actions: ['completeFeature'], + }, + }, + ]; + + testCases.forEach(testCase => { + test(`transition: ${testCase.event} for currentState ${testCase.currentState} and componentState ${ + testCase.params + }`, function(assert) { + let result = policiesMachine.transition(testCase.currentState, testCase.event, testCase.params); + assert.equal(result.value, testCase.expectedResults.value); + assert.deepEqual(result.actions, testCase.expectedResults.actions); + }); + }); +}); diff --git a/ui/tests/unit/machines/replication-machine-test.js b/ui/tests/unit/machines/replication-machine-test.js new file mode 100644 index 0000000000..c63ce23623 --- /dev/null +++ b/ui/tests/unit/machines/replication-machine-test.js @@ -0,0 +1,38 @@ +import { module, test } from 'qunit'; +import { Machine } from 'xstate'; +import ReplicationMachineConfig from 'vault/machines/replication-machine'; + +module('Unit | Machine | replication-machine', function() { + const replicationMachine = Machine(ReplicationMachineConfig); + + const testCases = [ + { + currentState: replicationMachine.initialState, + event: 'ENABLEREPLICATION', + params: null, + expectedResults: { + value: 'details', + actions: [{ type: 'render', level: 'feature', component: 'wizard/replication-details' }], + }, + }, + { + currentState: 'details', + event: 'CONTINUE', + params: null, + expectedResults: { + value: 'complete', + actions: ['completeFeature'], + }, + }, + ]; + + testCases.forEach(testCase => { + test(`transition: ${testCase.event} for currentState ${testCase.currentState} and componentState ${ + testCase.params + }`, function(assert) { + let result = replicationMachine.transition(testCase.currentState, testCase.event, testCase.params); + assert.equal(result.value, testCase.expectedResults.value); + assert.deepEqual(result.actions, testCase.expectedResults.actions); + }); + }); +}); diff --git a/ui/tests/unit/machines/secrets-machine-test.js b/ui/tests/unit/machines/secrets-machine-test.js new file mode 100644 index 0000000000..d3c06135ee --- /dev/null +++ b/ui/tests/unit/machines/secrets-machine-test.js @@ -0,0 +1,1072 @@ +import { module, test } from 'qunit'; +import { Machine } from 'xstate'; +import SecretsMachineConfig from 'vault/machines/secrets-machine'; + +module('Unit | Machine | secrets-machine', function() { + const secretsMachine = Machine(SecretsMachineConfig); + + const testCases = [ + { + currentState: secretsMachine.initialState, + event: 'CONTINUE', + params: null, + expectedResults: { + value: 'enable', + actions: [ + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + { component: 'wizard/secrets-enable', level: 'step', type: 'render' }, + ], + }, + }, + { + currentState: 'enable', + event: 'CONTINUE', + params: 'aws', + expectedResults: { + value: 'details', + actions: [ + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + { component: 'wizard/secrets-details', level: 'step', type: 'render' }, + ], + }, + }, + { + currentState: 'details', + event: 'CONTINUE', + params: 'aws', + expectedResults: { + value: 'role', + actions: [ + { component: 'wizard/secrets-role', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'role', + event: 'CONTINUE', + params: 'aws', + expectedResults: { + value: 'displayRole', + actions: [ + { component: 'wizard/secrets-display-role', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'displayRole', + event: 'CONTINUE', + params: 'aws', + expectedResults: { + value: 'credentials', + actions: [ + { component: 'wizard/secrets-credentials', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'credentials', + event: 'CONTINUE', + params: 'aws', + expectedResults: { + value: 'display', + actions: [ + { component: 'wizard/secrets-display', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'display', + event: 'REPEAT', + params: 'aws', + expectedResults: { + value: 'role', + actions: [ + { + params: ['vault.cluster.secrets.backend.create-root'], + type: 'routeTransition', + }, + { component: 'wizard/secrets-role', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'display', + event: 'RESET', + params: 'aws', + expectedResults: { + value: 'idle', + actions: [ + { + params: ['vault.cluster.settings.mount-secret-backend'], + type: 'routeTransition', + }, + { + component: 'wizard/mounts-wizard', + level: 'feature', + type: 'render', + }, + { + component: 'wizard/secrets-idle', + level: 'step', + type: 'render', + }, + ], + }, + }, + { + currentState: 'display', + event: 'DONE', + params: 'aws', + expectedResults: { + value: 'complete', + actions: ['completeFeature'], + }, + }, + { + currentState: 'display', + event: 'ERROR', + params: 'aws', + expectedResults: { + value: 'error', + actions: [ + { component: 'wizard/tutorial-error', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'enable', + event: 'CONTINUE', + params: 'pki', + expectedResults: { + value: 'details', + actions: [ + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + { component: 'wizard/secrets-details', level: 'step', type: 'render' }, + ], + }, + }, + { + currentState: 'details', + event: 'CONTINUE', + params: 'pki', + expectedResults: { + value: 'role', + actions: [ + { component: 'wizard/secrets-role', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'role', + event: 'CONTINUE', + params: 'pki', + expectedResults: { + value: 'displayRole', + actions: [ + { component: 'wizard/secrets-display-role', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'displayRole', + event: 'CONTINUE', + params: 'pki', + expectedResults: { + value: 'credentials', + actions: [ + { component: 'wizard/secrets-credentials', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'credentials', + event: 'CONTINUE', + params: 'pki', + expectedResults: { + value: 'display', + actions: [ + { component: 'wizard/secrets-display', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'display', + event: 'REPEAT', + params: 'pki', + expectedResults: { + value: 'role', + actions: [ + { + params: ['vault.cluster.secrets.backend.create-root'], + type: 'routeTransition', + }, + { component: 'wizard/secrets-role', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'display', + event: 'RESET', + params: 'pki', + expectedResults: { + value: 'idle', + actions: [ + { + params: ['vault.cluster.settings.mount-secret-backend'], + type: 'routeTransition', + }, + { + component: 'wizard/mounts-wizard', + level: 'feature', + type: 'render', + }, + { + component: 'wizard/secrets-idle', + level: 'step', + type: 'render', + }, + ], + }, + }, + { + currentState: 'display', + event: 'DONE', + params: 'pki', + expectedResults: { + value: 'complete', + actions: ['completeFeature'], + }, + }, + { + currentState: 'display', + event: 'ERROR', + params: 'pki', + expectedResults: { + value: 'error', + actions: [ + { component: 'wizard/tutorial-error', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'enable', + event: 'CONTINUE', + params: 'ssh', + expectedResults: { + value: 'details', + actions: [ + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + { component: 'wizard/secrets-details', level: 'step', type: 'render' }, + ], + }, + }, + { + currentState: 'details', + event: 'CONTINUE', + params: 'ssh', + expectedResults: { + value: 'role', + actions: [ + { component: 'wizard/secrets-role', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'role', + event: 'CONTINUE', + params: 'ssh', + expectedResults: { + value: 'displayRole', + actions: [ + { component: 'wizard/secrets-display-role', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'displayRole', + event: 'CONTINUE', + params: 'ssh', + expectedResults: { + value: 'credentials', + actions: [ + { component: 'wizard/secrets-credentials', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'credentials', + event: 'CONTINUE', + params: 'ssh', + expectedResults: { + value: 'display', + actions: [ + { component: 'wizard/secrets-display', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'display', + event: 'REPEAT', + params: 'ssh', + expectedResults: { + value: 'role', + actions: [ + { + params: ['vault.cluster.secrets.backend.create-root'], + type: 'routeTransition', + }, + { component: 'wizard/secrets-role', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'display', + event: 'RESET', + params: 'ssh', + expectedResults: { + value: 'idle', + actions: [ + { + params: ['vault.cluster.settings.mount-secret-backend'], + type: 'routeTransition', + }, + { + component: 'wizard/mounts-wizard', + level: 'feature', + type: 'render', + }, + { + component: 'wizard/secrets-idle', + level: 'step', + type: 'render', + }, + ], + }, + }, + { + currentState: 'display', + event: 'DONE', + params: 'ssh', + expectedResults: { + value: 'complete', + actions: ['completeFeature'], + }, + }, + { + currentState: 'display', + event: 'ERROR', + params: 'ssh', + expectedResults: { + value: 'error', + actions: [ + { component: 'wizard/tutorial-error', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'enable', + event: 'CONTINUE', + params: 'ad', + expectedResults: { + value: 'list', + actions: [ + { type: 'render', level: 'step', component: 'wizard/secrets-list' }, + { type: 'render', level: 'feature', component: 'wizard/mounts-wizard' }, + ], + }, + }, + { + currentState: 'list', + event: 'CONTINUE', + params: 'ad', + expectedResults: { + value: 'display', + actions: [ + { component: 'wizard/secrets-display', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'display', + event: 'RESET', + params: 'ad', + expectedResults: { + value: 'idle', + actions: [ + { + params: ['vault.cluster.settings.mount-secret-backend'], + type: 'routeTransition', + }, + { + component: 'wizard/mounts-wizard', + level: 'feature', + type: 'render', + }, + { + component: 'wizard/secrets-idle', + level: 'step', + type: 'render', + }, + ], + }, + }, + { + currentState: 'display', + event: 'DONE', + params: 'ad', + expectedResults: { + value: 'complete', + actions: ['completeFeature'], + }, + }, + { + currentState: 'display', + event: 'ERROR', + params: 'ad', + expectedResults: { + value: 'error', + actions: [ + { component: 'wizard/tutorial-error', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'enable', + event: 'CONTINUE', + params: 'consul', + expectedResults: { + value: 'list', + actions: [ + { type: 'render', level: 'step', component: 'wizard/secrets-list' }, + { type: 'render', level: 'feature', component: 'wizard/mounts-wizard' }, + ], + }, + }, + { + currentState: 'list', + event: 'CONTINUE', + params: 'consul', + expectedResults: { + value: 'display', + actions: [ + { component: 'wizard/secrets-display', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'display', + event: 'RESET', + params: 'consul', + expectedResults: { + value: 'idle', + actions: [ + { + params: ['vault.cluster.settings.mount-secret-backend'], + type: 'routeTransition', + }, + { + component: 'wizard/mounts-wizard', + level: 'feature', + type: 'render', + }, + { + component: 'wizard/secrets-idle', + level: 'step', + type: 'render', + }, + ], + }, + }, + { + currentState: 'display', + event: 'DONE', + params: 'consul', + expectedResults: { + value: 'complete', + actions: ['completeFeature'], + }, + }, + { + currentState: 'display', + event: 'ERROR', + params: 'consul', + expectedResults: { + value: 'error', + actions: [ + { component: 'wizard/tutorial-error', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'enable', + event: 'CONTINUE', + params: 'database', + expectedResults: { + value: 'list', + actions: [ + { type: 'render', level: 'step', component: 'wizard/secrets-list' }, + { type: 'render', level: 'feature', component: 'wizard/mounts-wizard' }, + ], + }, + }, + { + currentState: 'list', + event: 'CONTINUE', + params: 'database', + expectedResults: { + value: 'display', + actions: [ + { component: 'wizard/secrets-display', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'display', + event: 'RESET', + params: 'database', + expectedResults: { + value: 'idle', + actions: [ + { + params: ['vault.cluster.settings.mount-secret-backend'], + type: 'routeTransition', + }, + { + component: 'wizard/mounts-wizard', + level: 'feature', + type: 'render', + }, + { + component: 'wizard/secrets-idle', + level: 'step', + type: 'render', + }, + ], + }, + }, + { + currentState: 'display', + event: 'DONE', + params: 'database', + expectedResults: { + value: 'complete', + actions: ['completeFeature'], + }, + }, + { + currentState: 'display', + event: 'ERROR', + params: 'database', + expectedResults: { + value: 'error', + actions: [ + { component: 'wizard/tutorial-error', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'enable', + event: 'CONTINUE', + params: 'gcp', + expectedResults: { + value: 'list', + actions: [ + { type: 'render', level: 'step', component: 'wizard/secrets-list' }, + { type: 'render', level: 'feature', component: 'wizard/mounts-wizard' }, + ], + }, + }, + { + currentState: 'list', + event: 'CONTINUE', + params: 'gcp', + expectedResults: { + value: 'display', + actions: [ + { component: 'wizard/secrets-display', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'display', + event: 'RESET', + params: 'gcp', + expectedResults: { + value: 'idle', + actions: [ + { + params: ['vault.cluster.settings.mount-secret-backend'], + type: 'routeTransition', + }, + { + component: 'wizard/mounts-wizard', + level: 'feature', + type: 'render', + }, + { + component: 'wizard/secrets-idle', + level: 'step', + type: 'render', + }, + ], + }, + }, + { + currentState: 'display', + event: 'DONE', + params: 'gcp', + expectedResults: { + value: 'complete', + actions: ['completeFeature'], + }, + }, + { + currentState: 'display', + event: 'ERROR', + params: 'gcp', + expectedResults: { + value: 'error', + actions: [ + { component: 'wizard/tutorial-error', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'enable', + event: 'CONTINUE', + params: 'nomad', + expectedResults: { + value: 'list', + actions: [ + { type: 'render', level: 'step', component: 'wizard/secrets-list' }, + { type: 'render', level: 'feature', component: 'wizard/mounts-wizard' }, + ], + }, + }, + { + currentState: 'list', + event: 'CONTINUE', + params: 'nomad', + expectedResults: { + value: 'display', + actions: [ + { component: 'wizard/secrets-display', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'display', + event: 'RESET', + params: 'nomad', + expectedResults: { + value: 'idle', + actions: [ + { + params: ['vault.cluster.settings.mount-secret-backend'], + type: 'routeTransition', + }, + { + component: 'wizard/mounts-wizard', + level: 'feature', + type: 'render', + }, + { + component: 'wizard/secrets-idle', + level: 'step', + type: 'render', + }, + ], + }, + }, + { + currentState: 'display', + event: 'DONE', + params: 'nomad', + expectedResults: { + value: 'complete', + actions: ['completeFeature'], + }, + }, + { + currentState: 'display', + event: 'ERROR', + params: 'nomad', + expectedResults: { + value: 'error', + actions: [ + { component: 'wizard/tutorial-error', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'enable', + event: 'CONTINUE', + params: 'rabbitmq', + expectedResults: { + value: 'list', + actions: [ + { type: 'render', level: 'step', component: 'wizard/secrets-list' }, + { type: 'render', level: 'feature', component: 'wizard/mounts-wizard' }, + ], + }, + }, + { + currentState: 'list', + event: 'CONTINUE', + params: 'rabbitmq', + expectedResults: { + value: 'display', + actions: [ + { component: 'wizard/secrets-display', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'display', + event: 'RESET', + params: 'rabbitmq', + expectedResults: { + value: 'idle', + actions: [ + { + params: ['vault.cluster.settings.mount-secret-backend'], + type: 'routeTransition', + }, + { + component: 'wizard/mounts-wizard', + level: 'feature', + type: 'render', + }, + { + component: 'wizard/secrets-idle', + level: 'step', + type: 'render', + }, + ], + }, + }, + { + currentState: 'display', + event: 'DONE', + params: 'rabbitmq', + expectedResults: { + value: 'complete', + actions: ['completeFeature'], + }, + }, + { + currentState: 'display', + event: 'ERROR', + params: 'rabbitmq', + expectedResults: { + value: 'error', + actions: [ + { component: 'wizard/tutorial-error', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'enable', + event: 'CONTINUE', + params: 'totp', + expectedResults: { + value: 'list', + actions: [ + { type: 'render', level: 'step', component: 'wizard/secrets-list' }, + { type: 'render', level: 'feature', component: 'wizard/mounts-wizard' }, + ], + }, + }, + { + currentState: 'list', + event: 'CONTINUE', + params: 'totp', + expectedResults: { + value: 'display', + actions: [ + { component: 'wizard/secrets-display', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'display', + event: 'RESET', + params: 'totp', + expectedResults: { + value: 'idle', + actions: [ + { + params: ['vault.cluster.settings.mount-secret-backend'], + type: 'routeTransition', + }, + { + component: 'wizard/mounts-wizard', + level: 'feature', + type: 'render', + }, + { + component: 'wizard/secrets-idle', + level: 'step', + type: 'render', + }, + ], + }, + }, + { + currentState: 'display', + event: 'DONE', + params: 'totp', + expectedResults: { + value: 'complete', + actions: ['completeFeature'], + }, + }, + { + currentState: 'display', + event: 'ERROR', + params: 'totp', + expectedResults: { + value: 'error', + actions: [ + { component: 'wizard/tutorial-error', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'enable', + event: 'CONTINUE', + params: 'kv', + expectedResults: { + value: 'details', + actions: [ + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + { component: 'wizard/secrets-details', level: 'step', type: 'render' }, + ], + }, + }, + { + currentState: 'details', + event: 'CONTINUE', + params: 'kv', + expectedResults: { + value: 'secret', + actions: [ + { component: 'wizard/secrets-secret', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'secret', + event: 'CONTINUE', + params: 'kv', + expectedResults: { + value: 'display', + actions: [ + { component: 'wizard/secrets-display', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'display', + event: 'REPEAT', + params: 'kv', + expectedResults: { + value: 'secret', + actions: [ + { + params: ['vault.cluster.secrets.backend.create-root'], + type: 'routeTransition', + }, + { component: 'wizard/secrets-secret', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'display', + event: 'RESET', + params: 'kv', + expectedResults: { + value: 'idle', + actions: [ + { + params: ['vault.cluster.settings.mount-secret-backend'], + type: 'routeTransition', + }, + { + component: 'wizard/mounts-wizard', + level: 'feature', + type: 'render', + }, + { + component: 'wizard/secrets-idle', + level: 'step', + type: 'render', + }, + ], + }, + }, + { + currentState: 'display', + event: 'DONE', + params: 'kv', + expectedResults: { + value: 'complete', + actions: ['completeFeature'], + }, + }, + { + currentState: 'display', + event: 'ERROR', + params: 'kv', + expectedResults: { + value: 'error', + actions: [ + { component: 'wizard/tutorial-error', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'enable', + event: 'CONTINUE', + params: 'transit', + expectedResults: { + value: 'details', + actions: [ + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + { component: 'wizard/secrets-details', level: 'step', type: 'render' }, + ], + }, + }, + { + currentState: 'details', + event: 'CONTINUE', + params: 'transit', + expectedResults: { + value: 'encryption', + actions: [ + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + { component: 'wizard/secrets-encryption', level: 'step', type: 'render' }, + ], + }, + }, + { + currentState: 'encryption', + event: 'CONTINUE', + params: 'transit', + expectedResults: { + value: 'display', + actions: [ + { component: 'wizard/secrets-display', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + { + currentState: 'display', + event: 'REPEAT', + params: 'transit', + expectedResults: { + value: 'encryption', + actions: [ + { + params: ['vault.cluster.secrets.backend.create-root'], + type: 'routeTransition', + }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + { component: 'wizard/secrets-encryption', level: 'step', type: 'render' }, + ], + }, + }, + { + currentState: 'display', + event: 'RESET', + params: 'transit', + expectedResults: { + value: 'idle', + actions: [ + { + params: ['vault.cluster.settings.mount-secret-backend'], + type: 'routeTransition', + }, + { + component: 'wizard/mounts-wizard', + level: 'feature', + type: 'render', + }, + { + component: 'wizard/secrets-idle', + level: 'step', + type: 'render', + }, + ], + }, + }, + { + currentState: 'display', + event: 'DONE', + params: 'transit', + expectedResults: { + value: 'complete', + actions: ['completeFeature'], + }, + }, + { + currentState: 'display', + event: 'ERROR', + params: 'transit', + expectedResults: { + value: 'error', + actions: [ + { component: 'wizard/tutorial-error', level: 'step', type: 'render' }, + { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, + ], + }, + }, + ]; + + testCases.forEach(testCase => { + test(`transition: ${testCase.event} for currentState ${testCase.currentState} and componentState ${ + testCase.params + }`, function(assert) { + let result = secretsMachine.transition(testCase.currentState, testCase.event, testCase.params); + assert.equal(result.value, testCase.expectedResults.value); + assert.deepEqual(result.actions, testCase.expectedResults.actions); + }); + }); +}); diff --git a/ui/tests/unit/machines/tools-machine-test.js b/ui/tests/unit/machines/tools-machine-test.js new file mode 100644 index 0000000000..b5d9fcd72f --- /dev/null +++ b/ui/tests/unit/machines/tools-machine-test.js @@ -0,0 +1,91 @@ +import { module, test } from 'qunit'; +import { Machine } from 'xstate'; +import ToolsMachineConfig from 'vault/machines/tools-machine'; + +module('Unit | Machine | tools-machine', function() { + const toolsMachine = Machine(ToolsMachineConfig); + + const testCases = [ + { + currentState: toolsMachine.initialState, + event: 'CONTINUE', + params: null, + expectedResults: { + value: 'wrapped', + actions: [{ type: 'render', level: 'feature', component: 'wizard/tools-wrapped' }], + }, + }, + { + currentState: 'wrapped', + event: 'LOOKUP', + params: null, + expectedResults: { + value: 'lookup', + actions: [{ type: 'render', level: 'feature', component: 'wizard/tools-lookup' }], + }, + }, + { + currentState: 'lookup', + event: 'CONTINUE', + params: null, + expectedResults: { + value: 'info', + actions: [{ type: 'render', level: 'feature', component: 'wizard/tools-info' }], + }, + }, + { + currentState: 'info', + event: 'REWRAP', + params: null, + expectedResults: { + value: 'rewrap', + actions: [{ type: 'render', level: 'feature', component: 'wizard/tools-rewrap' }], + }, + }, + { + currentState: 'rewrap', + event: 'CONTINUE', + params: null, + expectedResults: { + value: 'rewrapped', + actions: [{ type: 'render', level: 'feature', component: 'wizard/tools-rewrapped' }], + }, + }, + { + currentState: 'rewrapped', + event: 'UNWRAP', + params: null, + expectedResults: { + value: 'unwrap', + actions: [{ type: 'render', level: 'feature', component: 'wizard/tools-unwrap' }], + }, + }, + { + currentState: 'unwrap', + event: 'CONTINUE', + params: null, + expectedResults: { + value: 'unwrapped', + actions: [{ type: 'render', level: 'feature', component: 'wizard/tools-unwrapped' }], + }, + }, + { + currentState: 'unwrapped', + event: 'CONTINUE', + expectedResults: { + value: 'complete', + actions: ['completeFeature'], + }, + }, + ]; + + testCases.forEach(testCase => { + test(`transition: ${testCase.event} for currentState ${testCase.currentState} and componentState ${ + testCase.params + }`, function(assert) { + let result = toolsMachine.transition(testCase.currentState, testCase.event, testCase.params); + assert.equal(result.value, testCase.expectedResults.value); + assert.deepEqual(result.actions, testCase.expectedResults.actions); + }); + }); +}); diff --git a/ui/tests/unit/machines/tutorial-machine-test.js b/ui/tests/unit/machines/tutorial-machine-test.js new file mode 100644 index 0000000000..aa4e33c469 --- /dev/null +++ b/ui/tests/unit/machines/tutorial-machine-test.js @@ -0,0 +1,247 @@ +import { module, test } from 'qunit'; +import { Machine } from 'xstate'; +import TutorialMachineConfig from 'vault/machines/tutorial-machine'; + +module('Unit | Machine | tutorial-machine', function() { + const tutorialMachine = Machine(TutorialMachineConfig); + + const testCases = [ + { + currentState: 'init', + event: 'START', + params: null, + expectedResults: { + value: { + init: { + active: 'setup', + }, + }, + actions: [ + { type: 'render', level: 'tutorial', component: 'wizard/tutorial-active' }, + { type: 'render', level: 'feature', component: 'wizard/init-setup' }, + ], + }, + }, + { + currentState: 'init.active.setup', + event: 'TOSAVE', + params: null, + expectedResults: { + value: { + init: { + active: 'save', + }, + }, + actions: [{ type: 'render', level: 'feature', component: 'wizard/init-save-keys' }], + }, + }, + { + currentState: 'init', + event: 'SAVE', + params: null, + expectedResults: { + value: { + init: { + active: 'save', + }, + }, + actions: [ + { type: 'render', level: 'tutorial', component: 'wizard/tutorial-active' }, + { type: 'render', level: 'feature', component: 'wizard/init-save-keys' }, + ], + }, + }, + { + currentState: 'init.active.save', + event: 'TOUNSEAL', + params: null, + expectedResults: { + value: { + init: { + active: 'unseal', + }, + }, + actions: [{ type: 'render', level: 'feature', component: 'wizard/init-unseal' }], + }, + }, + { + currentState: 'init', + event: 'UNSEAL', + params: null, + expectedResults: { + value: { + init: { + active: 'unseal', + }, + }, + actions: [ + { type: 'render', level: 'tutorial', component: 'wizard/tutorial-active' }, + { type: 'render', level: 'feature', component: 'wizard/init-unseal' }, + ], + }, + }, + { + currentState: 'init.active.unseal', + event: 'TOLOGIN', + params: null, + expectedResults: { + value: { + init: { + active: 'login', + }, + }, + actions: [{ type: 'render', level: 'feature', component: 'wizard/init-login' }], + }, + }, + { + currentState: 'init', + event: 'LOGIN', + params: null, + expectedResults: { + value: { + init: { + active: 'login', + }, + }, + actions: [ + { type: 'render', level: 'tutorial', component: 'wizard/tutorial-active' }, + { type: 'render', level: 'feature', component: 'wizard/init-login' }, + ], + }, + }, + { + currentState: 'init.active.login', + event: 'INITDONE', + params: null, + expectedResults: { + value: { + active: 'select', + }, + actions: [ + 'showTutorialWhenAuthenticated', + { type: 'render', level: 'tutorial', component: 'wizard/tutorial-active' }, + { type: 'render', level: 'feature', component: 'wizard/features-selection' }, + ], + }, + }, + { + currentState: 'active.select', + event: 'CONTINUE', + params: null, + expectedResults: { + value: { + active: 'feature', + }, + actions: [], + }, + }, + { + currentState: 'active.feature', + event: 'DISMISS', + params: null, + expectedResults: { + value: 'dismissed', + actions: [ + { type: 'render', level: 'tutorial', component: null }, + { type: 'render', level: 'feature', component: null }, + { type: 'render', level: 'step', component: null }, + { type: 'render', level: 'detail', component: null }, + 'handleDismissed', + ], + }, + }, + { + currentState: 'active.feature', + event: 'DONE', + params: null, + expectedResults: { + value: 'complete', + actions: [ + { type: 'render', level: 'feature', component: null }, + { type: 'render', level: 'step', component: null }, + { type: 'render', level: 'detail', component: null }, + { type: 'render', level: 'tutorial', component: 'wizard/tutorial-complete' }, + ], + }, + }, + { + currentState: 'active.feature', + event: 'PAUSE', + params: null, + expectedResults: { + value: 'paused', + actions: [ + { type: 'render', level: 'feature', component: null }, + { type: 'render', level: 'step', component: null }, + { type: 'render', level: 'detail', component: null }, + { type: 'render', level: 'tutorial', component: 'wizard/tutorial-paused' }, + 'handlePaused', + ], + }, + }, + { + currentState: 'paused', + event: 'CONTINUE', + params: null, + expectedResults: { + value: { + active: 'feature', + }, + actions: ['handleResume', { type: 'render', level: 'tutorial', component: 'wizard/tutorial-active' }], + }, + }, + { + currentState: 'idle', + event: 'INIT', + params: null, + expectedResults: { + value: { + init: 'idle', + }, + actions: [ + 'showTutorialAlways', + { type: 'render', level: 'tutorial', component: 'wizard/tutorial-idle' }, + { type: 'render', level: 'feature', component: null }, + ], + }, + }, + { + currentState: 'idle', + event: 'AUTH', + params: null, + expectedResults: { + value: { + active: 'select', + }, + actions: [ + { type: 'render', level: 'tutorial', component: 'wizard/tutorial-active' }, + { type: 'render', level: 'feature', component: 'wizard/features-selection' }, + ], + }, + }, + { + currentState: 'idle', + event: 'CONTINUE', + params: null, + expectedResults: { + value: { + active: 'select', + }, + actions: [ + { type: 'render', level: 'tutorial', component: 'wizard/tutorial-active' }, + { type: 'render', level: 'feature', component: 'wizard/features-selection' }, + ], + }, + }, + ]; + + testCases.forEach(testCase => { + test(`transition: ${testCase.event} for currentState ${testCase.currentState} and componentState ${ + testCase.params + }`, function(assert) { + let result = tutorialMachine.transition(testCase.currentState, testCase.event, testCase.params); + assert.deepEqual(result.value, testCase.expectedResults.value); + assert.deepEqual(result.actions, testCase.expectedResults.actions); + }); + }); +}); diff --git a/ui/tests/unit/services/wizard-test.js b/ui/tests/unit/services/wizard-test.js new file mode 100644 index 0000000000..a78e8ba5f1 --- /dev/null +++ b/ui/tests/unit/services/wizard-test.js @@ -0,0 +1,258 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import Service from '@ember/service'; +import sinon from 'sinon'; +import { STORAGE_KEYS, DEFAULTS } from 'vault/helpers/wizard-constants'; + +let routerStub = Service.extend({ + transitionTo: sinon.stub().returns({ + followRedirects: function() { + return { + then: function(callback) { + callback(); + }, + }; + }, + }), + urlFor: sinon.stub().returns('/ui/vault/foo'), +}); + +module('Unit | Service | wizard', function(hooks) { + setupTest(hooks); + + hooks.beforeEach(function() { + this.owner.register('service:router', routerStub); + this.router = this.owner.lookup('service:router'); + }); + + function storage() { + return { + items: {}, + getItem(key) { + var item = this.items[key]; + return item && JSON.parse(item); + }, + + setItem(key, val) { + return (this.items[key] = JSON.stringify(val)); + }, + + removeItem(key) { + delete this.items[key]; + }, + + keys() { + return Object.keys(this.items); + }, + }; + } + + let testCases = [ + { + method: 'getExtState', + args: [STORAGE_KEYS.TUTORIAL_STATE], + expectedResults: { + storage: [{ key: STORAGE_KEYS.TUTORIAL_STATE, value: 'idle' }], + }, + }, + { + method: 'saveExtState', + args: [STORAGE_KEYS.TUTORIAL_STATE, 'test'], + expectedResults: { + storage: [{ key: STORAGE_KEYS.TUTORIAL_STATE, value: 'test' }], + }, + }, + { + method: 'storageHasKey', + args: ['fake-key'], + expectedResults: { value: false }, + }, + { + method: 'storageHasKey', + args: [STORAGE_KEYS.TUTORIAL_STATE], + expectedResults: { value: true }, + }, + { + method: 'handleDismissed', + args: [], + expectedResults: { + storage: [ + { key: STORAGE_KEYS.FEATURE_STATE, value: undefined }, + { key: STORAGE_KEYS.FEATURE_LIST, value: undefined }, + { key: STORAGE_KEYS.COMPONENT_STATE, value: undefined }, + ], + }, + }, + { + method: 'handlePaused', + args: [], + properties: { + expectedURL: 'this/is/a/url', + expectedRouteName: 'this.is.a.route', + }, + expectedResults: { + storage: [ + { key: STORAGE_KEYS.RESUME_URL, value: 'this/is/a/url' }, + { key: STORAGE_KEYS.RESUME_ROUTE, value: 'this.is.a.route' }, + ], + }, + }, + { + method: 'handlePaused', + args: [], + expectedResults: { + storage: [ + { key: STORAGE_KEYS.RESUME_URL, value: undefined }, + { key: STORAGE_KEYS.RESUME_ROUTE, value: undefined }, + ], + }, + }, + { + method: 'handleResume', + storage: [ + { key: STORAGE_KEYS.RESUME_URL, value: 'this/is/a/url' }, + { key: STORAGE_KEYS.RESUME_ROUTE, value: 'this.is.a.route' }, + ], + args: [], + expectedResults: { + props: [ + { prop: 'expectedURL', value: 'this/is/a/url' }, + { prop: 'expectedRouteName', value: 'this.is.a.route' }, + ], + storage: [ + { key: STORAGE_KEYS.RESUME_URL, value: undefined }, + { key: STORAGE_KEYS.RESUME_ROUTE, value: 'this.is.a.route' }, + ], + }, + }, + { + method: 'handleResume', + args: [], + expectedResults: { + storage: [ + { key: STORAGE_KEYS.RESUME_URL, value: undefined }, + { key: STORAGE_KEYS.RESUME_ROUTE, value: undefined }, + ], + }, + }, + { + method: 'restartGuide', + args: [], + expectedResults: { + props: [ + { prop: 'currentState', value: 'active.select' }, + { prop: 'featureComponent', value: 'wizard/features-selection' }, + { prop: 'tutorialComponent', value: 'wizard/tutorial-active' }, + ], + storage: [ + { key: STORAGE_KEYS.FEATURE_STATE, value: undefined }, + { key: STORAGE_KEYS.FEATURE_LIST, value: undefined }, + { key: STORAGE_KEYS.COMPONENT_STATE, value: undefined }, + { key: STORAGE_KEYS.TUTORIAL_STATE, value: 'active.select' }, + { key: STORAGE_KEYS.COMPLETED_FEATURES, value: undefined }, + { key: STORAGE_KEYS.RESUME_URL, value: undefined }, + { key: STORAGE_KEYS.RESUME_ROUTE, value: undefined }, + ], + }, + }, + { + method: 'saveState', + args: [ + 'currentState', + { + value: { + init: { + active: 'login', + }, + }, + actions: [{ type: 'render', level: 'feature', component: 'wizard/init-login' }], + }, + ], + expectedResults: { + props: [{ prop: 'currentState', value: 'init.active.login' }], + }, + }, + { + method: 'saveState', + args: [ + 'currentState', + { + value: { + active: 'login', + }, + actions: [{ type: 'render', level: 'feature', component: 'wizard/init-login' }], + }, + ], + expectedResults: { + props: [{ prop: 'currentState', value: 'active.login' }], + }, + }, + { + method: 'saveState', + args: ['currentState', 'login'], + expectedResults: { + props: [{ prop: 'currentState', value: 'login' }], + }, + }, + { + method: 'startFeature', + args: [], + properties: { featureList: ['secrets', 'tools'] }, + expectedResults: { + props: [{ prop: 'featureState', value: 'idle' }, { prop: 'currentMachine', value: 'secrets' }], + }, + }, + { + method: 'saveFeatures', + args: [['secrets', 'tools']], + expectedResults: { + props: [{ prop: 'featureList', value: ['secrets', 'tools'] }], + storage: [{ key: STORAGE_KEYS.FEATURE_LIST, value: ['secrets', 'tools'] }], + }, + }, + ]; + + testCases.forEach(testCase => { + let store = storage(); + test(`${testCase.method}`, function(assert) { + let wizard = this.owner.factoryFor('service:wizard').create({ + storage() { + return store; + }, + }); + + if (testCase.properties) { + wizard.setProperties(testCase.properties); + } else { + wizard.setProperties(DEFAULTS); + } + + if (testCase.storage) { + testCase.storage.forEach(item => wizard.storage().setItem(item.key, item.value)); + } + + let result = wizard[testCase.method](...testCase.args); + if (testCase.expectedResults.props) { + testCase.expectedResults.props.forEach(property => { + assert.deepEqual( + wizard.get(property.prop), + property.value, + `${testCase.method} creates correct value for ${property.prop}` + ); + }); + } + if (testCase.expectedResults.storage) { + testCase.expectedResults.storage.forEach(item => { + assert.deepEqual( + wizard.storage().getItem(item.key), + item.value, + `${testCase.method} creates correct storage state for ${item.key}` + ); + }); + } + if (testCase.expectedResults.value !== null && testCase.expectedResults.value !== undefined) { + assert.equal(result, testCase.expectedResults.value, `${testCase.method} gives correct value`); + } + }); + }); +});