+
{{:: vm.strings.get('inputs.GROUP_TITLE') }}
diff --git a/awx/ui/client/features/credentials/credentials.strings.js b/awx/ui/client/features/credentials/credentials.strings.js
index 958282aa09..cb6260ce55 100644
--- a/awx/ui/client/features/credentials/credentials.strings.js
+++ b/awx/ui/client/features/credentials/credentials.strings.js
@@ -17,7 +17,9 @@ function CredentialsStrings (BaseString) {
ns.inputs = {
GROUP_TITLE: t.s('Type Details'),
ORGANIZATION_PLACEHOLDER: t.s('SELECT AN ORGANIZATION'),
- CREDENTIAL_TYPE_PLACEHOLDER: t.s('SELECT A CREDENTIAL TYPE')
+ CREDENTIAL_TYPE_PLACEHOLDER: t.s('SELECT A CREDENTIAL TYPE'),
+ GCE_FILE_INPUT_LABEL: t.s('Service Account JSON File'),
+ GCE_FILE_INPUT_HELP_TEXT: t.s('Provide account information using Google Compute Engine JSON credentials file.')
};
ns.add = {
diff --git a/awx/ui/client/features/credentials/edit-credentials.controller.js b/awx/ui/client/features/credentials/edit-credentials.controller.js
index e0a36c106f..a7629a68e1 100644
--- a/awx/ui/client/features/credentials/edit-credentials.controller.js
+++ b/awx/ui/client/features/credentials/edit-credentials.controller.js
@@ -1,4 +1,4 @@
-function EditCredentialsController (models, $state, $scope, strings) {
+function EditCredentialsController (models, $state, $scope, strings, componentsStrings) {
const vm = this || {};
const { me, credential, credentialType, organization } = models;
@@ -64,15 +64,35 @@ function EditCredentialsController (models, $state, $scope, strings) {
vm.form.credential_type._displayValue = credentialType.get('name');
vm.form.credential_type._placeholder = strings.get('inputs.CREDENTIAL_TYPE_PLACEHOLDER');
+ const gceFileInputSchema = {
+ id: 'gce_service_account_key',
+ type: 'file',
+ label: strings.get('inputs.GCE_FILE_INPUT_LABEL'),
+ help_text: strings.get('inputs.GCE_FILE_INPUT_HELP_TEXT'),
+ };
+
+ let gceFileInputPreEditValues;
+
vm.form.inputs = {
_get () {
+ let fields;
+
credentialType.mergeInputProperties();
if (credentialType.get('id') === credential.get('credential_type')) {
- return credential.assignInputGroupValues(credentialType.get('inputs.fields'));
+ fields = credential.assignInputGroupValues(credentialType.get('inputs.fields'));
+ } else {
+ fields = credentialType.get('inputs.fields');
}
- return credentialType.get('inputs.fields');
+ if (credentialType.get('name') === 'Google Compute Engine') {
+ fields.splice(2, 0, gceFileInputSchema);
+
+ $scope.$watch(`vm.form.${gceFileInputSchema.id}._value`, vm.gceOnFileInputChanged);
+ $scope.$watch('vm.form.ssh_key_data._isBeingReplaced', vm.gceOnReplaceKeyChanged);
+ }
+
+ return fields;
},
_source: vm.form.credential_type,
_reference: 'vm.form.inputs',
@@ -88,19 +108,70 @@ function EditCredentialsController (models, $state, $scope, strings) {
data.user = me.get('id');
credential.unset('inputs');
+ delete data.inputs[gceFileInputSchema.id];
+
return credential.request('put', { data });
};
vm.form.onSaveSuccess = () => {
$state.go('credentials.edit', { credential_id: credential.get('id') }, { reload: true });
};
+
+ vm.gceOnReplaceKeyChanged = value => {
+ vm.form[gceFileInputSchema.id]._disabled = !value;
+ };
+
+ vm.gceOnFileInputChanged = (value, oldValue) => {
+ if (value === oldValue) return;
+
+ const gceFileIsLoaded = !!value;
+ const gceFileInputState = vm.form[gceFileInputSchema.id];
+ const { obj, error } = vm.gceParseFileInput(value);
+
+ gceFileInputState._isValid = !error;
+ gceFileInputState._message = error ? componentsStrings.get('message.INVALID_INPUT') : '';
+
+ vm.form.project._disabled = gceFileIsLoaded;
+ vm.form.username._disabled = gceFileIsLoaded;
+ vm.form.ssh_key_data._disabled = gceFileIsLoaded;
+ vm.form.ssh_key_data._displayHint = !vm.form.ssh_key_data._disabled;
+
+ if (gceFileIsLoaded) {
+ gceFileInputPreEditValues = Object.assign({}, {
+ project: vm.form.project._value,
+ ssh_key_data: vm.form.ssh_key_data._value,
+ username: vm.form.username._value
+ });
+ vm.form.project._value = _.get(obj, 'project_id', '');
+ vm.form.ssh_key_data._value = _.get(obj, 'private_key', '');
+ vm.form.username._value = _.get(obj, 'client_email', '');
+ } else {
+ vm.form.project._value = gceFileInputPreEditValues.project;
+ vm.form.ssh_key_data._value = gceFileInputPreEditValues.ssh_key_data;
+ vm.form.username._value = gceFileInputPreEditValues.username;
+ }
+ };
+
+ vm.gceParseFileInput = value => {
+ let obj;
+ let error;
+
+ try {
+ obj = angular.fromJson(value);
+ } catch (err) {
+ error = err;
+ }
+
+ return { obj, error };
+ };
}
EditCredentialsController.$inject = [
'resolvedModels',
'$state',
'$scope',
- 'CredentialsStrings'
+ 'CredentialsStrings',
+ 'ComponentsStrings'
];
export default EditCredentialsController;
diff --git a/awx/ui/client/lib/components/components.strings.js b/awx/ui/client/lib/components/components.strings.js
index db27701a46..cd4a573499 100644
--- a/awx/ui/client/lib/components/components.strings.js
+++ b/awx/ui/client/lib/components/components.strings.js
@@ -16,6 +16,10 @@ function ComponentsStrings (BaseString) {
INVALID_INPUT: t.s('Invalid input for this type.')
};
+ ns.file = {
+ PLACEHOLDER: t.s('CHOOSE A FILE')
+ };
+
ns.form = {
SUBMISSION_ERROR_TITLE: t.s('Unable to Submit'),
SUBMISSION_ERROR_MESSAGE: t.s('Unexpected server error. View the console for more information'),
diff --git a/awx/ui/client/lib/components/form/form.directive.js b/awx/ui/client/lib/components/form/form.directive.js
index 17b781debc..e2e2e746a4 100644
--- a/awx/ui/client/lib/components/form/form.directive.js
+++ b/awx/ui/client/lib/components/form/form.directive.js
@@ -38,6 +38,10 @@ function AtFormController (eventService, strings) {
component.category = category;
component.form = vm.state;
+ if (category === 'input') {
+ scope.state[component.state.id] = component.state;
+ }
+
vm.components.push(component);
};
@@ -189,6 +193,7 @@ function AtFormController (eventService, strings) {
for (let j = 0; j < vm.components.length; j++) {
if (components[i] === vm.components[j].state) {
vm.components.splice(j, 1);
+ delete scope.state[components[i].id];
break;
}
}
diff --git a/awx/ui/client/lib/components/index.js b/awx/ui/client/lib/components/index.js
index 426b9244cd..3033812739 100644
--- a/awx/ui/client/lib/components/index.js
+++ b/awx/ui/client/lib/components/index.js
@@ -5,6 +5,7 @@ import divider from '~components/utility/divider.directive';
import form from '~components/form/form.directive';
import formAction from '~components/form/action.directive';
import inputCheckbox from '~components/input/checkbox.directive';
+import inputFile from '~components/input/file.directive';
import inputGroup from '~components/input/group.directive';
import inputLabel from '~components/input/label.directive';
import inputLookup from '~components/input/lookup.directive';
@@ -41,6 +42,7 @@ angular
.directive('atForm', form)
.directive('atFormAction', formAction)
.directive('atInputCheckbox', inputCheckbox)
+ .directive('atInputFile', inputFile)
.directive('atInputGroup', inputGroup)
.directive('atInputLabel', inputLabel)
.directive('atInputLookup', inputLookup)
diff --git a/awx/ui/client/lib/components/input/_index.less b/awx/ui/client/lib/components/input/_index.less
index 087eaddfcb..b92692c34e 100644
--- a/awx/ui/client/lib/components/input/_index.less
+++ b/awx/ui/client/lib/components/input/_index.less
@@ -15,6 +15,10 @@
border-color: @at-color-input-focus;
}
+ &[readonly] {
+ background: @at-color-input-readonly;
+ }
+
&[disabled] {
background: @at-color-input-disabled;
}
diff --git a/awx/ui/client/lib/components/input/base.controller.js b/awx/ui/client/lib/components/input/base.controller.js
index 13fa825a5d..5c27d903f0 100644
--- a/awx/ui/client/lib/components/input/base.controller.js
+++ b/awx/ui/client/lib/components/input/base.controller.js
@@ -85,9 +85,7 @@ function BaseInputController (strings) {
vm.updateValidationState(result);
};
- vm.toggleRevertReplace = () => {
- scope.state._isBeingReplaced = !scope.state._isBeingReplaced;
-
+ vm.onRevertReplaceToggle = () => {
if (!scope.state._isBeingReplaced) {
scope.state._buttonText = vm.strings.get('REPLACE');
scope.state._disabled = true;
diff --git a/awx/ui/client/lib/components/input/file.directive.js b/awx/ui/client/lib/components/input/file.directive.js
new file mode 100644
index 0000000000..42aa2c4fba
--- /dev/null
+++ b/awx/ui/client/lib/components/input/file.directive.js
@@ -0,0 +1,94 @@
+const templateUrl = require('~components/input/file.partial.html');
+
+function atInputFileLink (scope, element, attrs, controllers) {
+ const formController = controllers[0];
+ const inputController = controllers[1];
+
+ if (scope.tab === '1') {
+ element.find('input')[0].focus();
+ }
+
+ inputController.init(scope, element, formController);
+}
+
+function AtInputFileController (baseInputController, eventService) {
+ const vm = this || {};
+
+ let input;
+ let scope;
+
+ vm.init = (_scope_, element, form) => {
+ baseInputController.call(vm, 'input', _scope_, element, form);
+
+ scope = _scope_;
+ input = element.find('input')[0]; // eslint-disable-line prefer-destructuring
+
+ vm.listeners = vm.setFileListeners(input);
+
+ vm.check();
+ };
+
+ vm.onButtonClick = () => {
+ if (scope.state._value) {
+ vm.removeFile();
+ } else {
+ input.click();
+ }
+ };
+
+ vm.setFileListeners = inputEl => eventService.addListeners([
+ [inputEl, 'change', event => vm.handleFileChangeEvent(inputEl, event)]
+ ]);
+
+ vm.handleFileChangeEvent = (element, event) => {
+ if (element.files.length > 0) {
+ const reader = new FileReader();
+
+ reader.onload = () => vm.readFile(reader, event);
+ reader.readAsText(element.files[0]);
+ } else {
+ scope.$apply(vm.removeFile);
+ }
+ };
+
+ vm.readFile = (reader, event) => {
+ scope.$apply(() => {
+ scope.state._value = reader.result;
+ scope.state._displayValue = event.target.files[0].name;
+
+ vm.check();
+ });
+ };
+
+ vm.removeFile = () => {
+ delete scope.state._value;
+ delete scope.state._displayValue;
+
+ input.value = '';
+ };
+}
+
+AtInputFileController.$inject = [
+ 'BaseInputController',
+ 'EventService'
+];
+
+function atInputFile () {
+ return {
+ restrict: 'E',
+ transclude: true,
+ replace: true,
+ require: ['^^atForm', 'atInputFile'],
+ templateUrl,
+ controller: AtInputFileController,
+ controllerAs: 'vm',
+ link: atInputFileLink,
+ scope: {
+ state: '=',
+ col: '@',
+ tab: '@'
+ }
+ };
+}
+
+export default atInputFile;
diff --git a/awx/ui/client/lib/components/input/file.partial.html b/awx/ui/client/lib/components/input/file.partial.html
new file mode 100644
index 0000000000..7e13fcc47e
--- /dev/null
+++ b/awx/ui/client/lib/components/input/file.partial.html
@@ -0,0 +1,25 @@
+
diff --git a/awx/ui/client/lib/components/input/group.directive.js b/awx/ui/client/lib/components/input/group.directive.js
index c94dd99f04..7e6f1f2426 100644
--- a/awx/ui/client/lib/components/input/group.directive.js
+++ b/awx/ui/client/lib/components/input/group.directive.js
@@ -99,6 +99,8 @@ function AtInputGroupController ($scope, $compile) {
config._component = 'at-input-number';
} else if (input.type === 'boolean') {
config._component = 'at-input-checkbox';
+ } else if (input.type === 'file') {
+ config._component = 'at-input-file';
} else if (input.choices) {
config._component = 'at-input-select';
config._format = 'array';
diff --git a/awx/ui/client/lib/components/input/text.directive.js b/awx/ui/client/lib/components/input/text.directive.js
index e8d514fc7e..d28f4c2640 100644
--- a/awx/ui/client/lib/components/input/text.directive.js
+++ b/awx/ui/client/lib/components/input/text.directive.js
@@ -14,10 +14,14 @@ function atInputTextLink (scope, element, attrs, controllers) {
function AtInputTextController (baseInputController) {
const vm = this || {};
- vm.init = (scope, element, form) => {
- baseInputController.call(vm, 'input', scope, element, form);
+ let scope;
+
+ vm.init = (_scope_, element, form) => {
+ baseInputController.call(vm, 'input', _scope_, element, form);
+ scope = _scope_;
vm.check();
+ scope.$watch('state._value', () => vm.check());
};
}
diff --git a/awx/ui/client/lib/components/input/text.partial.html b/awx/ui/client/lib/components/input/text.partial.html
index c5140df834..8a1a22137e 100644
--- a/awx/ui/client/lib/components/input/text.partial.html
+++ b/awx/ui/client/lib/components/input/text.partial.html
@@ -8,7 +8,6 @@
ng-attr-maxlength="{{ state.max_length || undefined }}"
ng-attr-tabindex="{{ tab || undefined }}"
ng-attr-placeholder="{{::state._placeholder || undefined }}"
- ng-change="vm.check()"
ng-disabled="state._disabled || form.disabled" />
diff --git a/awx/ui/client/lib/components/input/textarea-secret.directive.js b/awx/ui/client/lib/components/input/textarea-secret.directive.js
index 7e4cd964d6..2bcbe027d5 100644
--- a/awx/ui/client/lib/components/input/textarea-secret.directive.js
+++ b/awx/ui/client/lib/components/input/textarea-secret.directive.js
@@ -21,6 +21,7 @@ function AtInputTextareaSecretController (baseInputController, eventService) {
baseInputController.call(vm, 'input', _scope_, element, form);
scope = _scope_;
+
[textarea] = element.find('textarea');
if (scope.state.format === 'ssh_private_key') {
@@ -38,10 +39,15 @@ function AtInputTextareaSecretController (baseInputController, eventService) {
}
vm.check();
+
+ scope.$watch('state[state._activeModel]', () => vm.check());
+ scope.$watch('state._isBeingReplaced', () => vm.onIsBeingReplacedChanged());
};
- vm.toggle = () => {
- vm.toggleRevertReplace();
+ vm.onIsBeingReplacedChanged = () => {
+ if (!scope.state._touched) return;
+
+ vm.onRevertReplaceToggle();
if (scope.state._isBeingReplaced) {
scope.state._placeholder = '';
@@ -50,7 +56,10 @@ function AtInputTextareaSecretController (baseInputController, eventService) {
} else {
scope.state._displayHint = false;
scope.state._placeholder = vm.strings.get('ENCRYPTED');
- eventService.remove(vm.listeners);
+
+ if (vm.listeners) {
+ eventService.remove(vm.listeners);
+ }
}
};
diff --git a/awx/ui/client/lib/components/input/textarea-secret.partial.html b/awx/ui/client/lib/components/input/textarea-secret.partial.html
index d7823a1547..e716cbf48d 100644
--- a/awx/ui/client/lib/components/input/textarea-secret.partial.html
+++ b/awx/ui/client/lib/components/input/textarea-secret.partial.html
@@ -6,11 +6,12 @@
diff --git a/awx/ui/client/lib/components/modal/modal.directive.js b/awx/ui/client/lib/components/modal/modal.directive.js
index f6e5510ce5..e1d1126121 100644
--- a/awx/ui/client/lib/components/modal/modal.directive.js
+++ b/awx/ui/client/lib/components/modal/modal.directive.js
@@ -22,8 +22,8 @@ function AtModalController (eventService, strings) {
vm.strings = strings;
vm.init = (scope, el) => {
- [overlay] = el;
- [modal] = el.find('.at-Modal-window');
+ overlay = el[0]; // eslint-disable-line prefer-destructuring
+ modal = el.find('.at-Modal-window')[0]; // eslint-disable-line prefer-destructuring
vm.modal = scope[scope.ns].modal;
vm.modal.show = vm.show;
diff --git a/awx/ui/client/lib/theme/_variables.less b/awx/ui/client/lib/theme/_variables.less
index 5bdbec8fda..2ff0156021 100644
--- a/awx/ui/client/lib/theme/_variables.less
+++ b/awx/ui/client/lib/theme/_variables.less
@@ -131,6 +131,8 @@
@at-color-input-button: @at-gray-light-3x;
@at-color-input-button-hover: @at-gray-light-2x;
@at-color-input-disabled: @at-gray-light;
+@at-color-input-readonly: @at-color-input-background;
+
@at-color-input-error: @at-color-error;
@at-color-input-focus: @at-color-info;
@at-color-input-hint: @at-gray-dark-4x;
diff --git a/awx/ui/test/e2e/.eslintrc.js b/awx/ui/test/e2e/.eslintrc.js
index 7ce4f5483c..02959f42e3 100644
--- a/awx/ui/test/e2e/.eslintrc.js
+++ b/awx/ui/test/e2e/.eslintrc.js
@@ -1,5 +1,6 @@
module.exports = {
rules: {
- 'no-unused-expressions': 'off'
+ 'no-unused-expressions': 'off',
+ 'no-unused-vars': 'off',
}
};
diff --git a/awx/ui/test/e2e/fixtures.js b/awx/ui/test/e2e/fixtures.js
index 1cc8a6ea72..a0f886e074 100644
--- a/awx/ui/test/e2e/fixtures.js
+++ b/awx/ui/test/e2e/fixtures.js
@@ -1,5 +1,7 @@
import uuid from 'uuid';
+import { AWX_E2E_PASSWORD } from './settings';
+
import {
all,
get,
@@ -231,7 +233,7 @@ const getAuditor = (namespace = session) => getOrganization(namespace)
email: 'null@ansible.com',
is_superuser: false,
is_system_auditor: true,
- password: 'password'
+ password: AWX_E2E_PASSWORD
}));
const getUser = (namespace = session) => getOrganization(namespace)
@@ -243,7 +245,7 @@ const getUser = (namespace = session) => getOrganization(namespace)
email: 'null@ansible.com',
is_superuser: false,
is_system_auditor: false,
- password: 'password'
+ password: AWX_E2E_PASSWORD
}));
const getJobTemplateAdmin = (namespace = session) => {
@@ -259,7 +261,7 @@ const getJobTemplateAdmin = (namespace = session) => {
email: 'null@ansible.com',
is_superuser: false,
is_system_auditor: false,
- password: 'password'
+ password: AWX_E2E_PASSWORD
}));
const assignRolePromise = Promise.all([userPromise, rolePromise])
diff --git a/awx/ui/test/e2e/objects/credentials.js b/awx/ui/test/e2e/objects/credentials.js
index ed825d9426..ad3cd3bb91 100644
--- a/awx/ui/test/e2e/objects/credentials.js
+++ b/awx/ui/test/e2e/objects/credentials.js
@@ -67,6 +67,7 @@ const gce = createFormSection({
email: 'Service Account Email Address',
project: 'Project',
sshKeyData: 'RSA Private Key',
+ serviceAccountFile: 'Service Account JSON File'
}
});
diff --git a/awx/ui/test/e2e/objects/sections/createFormSection.js b/awx/ui/test/e2e/objects/sections/createFormSection.js
index 3a8ebb810b..97e4b73394 100644
--- a/awx/ui/test/e2e/objects/sections/createFormSection.js
+++ b/awx/ui/test/e2e/objects/sections/createFormSection.js
@@ -12,7 +12,7 @@ const inputContainerElements = {
popover: '.at-Popover-container',
yaml: 'input[type="radio", value="yaml"]',
json: 'input[type="radio", value="json"]',
- revert: 'a[class~="reset"]',
+ reset: 'a[class~="reset"]',
down: 'span[class^="fa-angle-down"]',
up: 'span[class^="fa-angle-up"]',
prompt: {
@@ -34,6 +34,14 @@ const inputContainerElements = {
off: {
locateStrategy: 'xpath',
selector: `.//button[${normalized}='off']`
+ },
+ replace: {
+ locateStrategy: 'xpath',
+ selector: `.//button[${normalized}='replace']`
+ },
+ revert: {
+ locateStrategy: 'xpath',
+ selector: `.//button[${normalized}='revert']`
}
};
diff --git a/awx/ui/test/e2e/tests/gce.alt.json b/awx/ui/test/e2e/tests/gce.alt.json
new file mode 100644
index 0000000000..1080223f07
--- /dev/null
+++ b/awx/ui/test/e2e/tests/gce.alt.json
@@ -0,0 +1,12 @@
+{
+ "type": "service_account",
+ "project_id": "test321",
+ "private_key_id": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ "private_key": "-----BEGIN PRIVATE KEY-----\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\nBBBBBBBBBBBBBBBBBBBBBBBB\n-----END PRIVATE KEY-----\n",
+ "client_email": "test321.iam.gserviceaccount.com",
+ "client_id": "321987654321987654321",
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+ "token_uri": "https://accounts.google.com/o/oauth2/token",
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
+ "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/test321%40test.iam.gserviceaccount.com"
+}
diff --git a/awx/ui/test/e2e/tests/gce.invalid.json b/awx/ui/test/e2e/tests/gce.invalid.json
new file mode 100644
index 0000000000..9977a2836c
--- /dev/null
+++ b/awx/ui/test/e2e/tests/gce.invalid.json
@@ -0,0 +1 @@
+invalid
diff --git a/awx/ui/test/e2e/tests/gce.json b/awx/ui/test/e2e/tests/gce.json
new file mode 100644
index 0000000000..dd3055f059
--- /dev/null
+++ b/awx/ui/test/e2e/tests/gce.json
@@ -0,0 +1,12 @@
+{
+ "type": "service_account",
+ "project_id": "test123",
+ "private_key_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "private_key": "-----BEGIN PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAA\n-----END PRIVATE KEY-----\n",
+ "client_email": "test123.iam.gserviceaccount.com",
+ "client_id": "123456789123456789123",
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+ "token_uri": "https://accounts.google.com/o/oauth2/token",
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
+ "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/test123%40test.iam.gserviceaccount.com"
+}
diff --git a/awx/ui/test/e2e/tests/gce.missing.json b/awx/ui/test/e2e/tests/gce.missing.json
new file mode 100644
index 0000000000..5d0d1e2968
--- /dev/null
+++ b/awx/ui/test/e2e/tests/gce.missing.json
@@ -0,0 +1,11 @@
+{
+ "type": "service_account",
+ "project_id": "test654",
+ "private_key_id": "cccccccccccccccccccccccccccccccccccccccc",
+ "private_key": "-----BEGIN PRIVATE KEY-----\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\nCCCCCCCCCCCCCCCCCCCCCCCC\n-----END PRIVATE KEY-----\n",
+ "client_id": "654321987654321987654",
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+ "token_uri": "https://accounts.google.com/o/oauth2/token",
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
+ "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/test654%40test.iam.gserviceaccount.com"
+}
diff --git a/awx/ui/test/e2e/tests/test-auditor-read-only-forms.js b/awx/ui/test/e2e/tests/test-auditor-read-only-forms.js
index f31d33e173..ecce0836c5 100644
--- a/awx/ui/test/e2e/tests/test-auditor-read-only-forms.js
+++ b/awx/ui/test/e2e/tests/test-auditor-read-only-forms.js
@@ -63,7 +63,7 @@ module.exports = {
inventories = client.page.inventories();
teams = client.page.teams();
- client.login(data.auditor.username, data.auditor.password);
+ client.login(data.auditor.username);
client.waitForAngular();
done();
diff --git a/awx/ui/test/e2e/tests/test-credentials-add-edit-gce-file.js b/awx/ui/test/e2e/tests/test-credentials-add-edit-gce-file.js
new file mode 100644
index 0000000000..458b735630
--- /dev/null
+++ b/awx/ui/test/e2e/tests/test-credentials-add-edit-gce-file.js
@@ -0,0 +1,269 @@
+import path from 'path';
+import uuid from 'uuid';
+
+const GCE_SERVICE_ACCOUNT_FILE = path.resolve(__dirname, 'gce.json');
+const GCE_SERVICE_ACCOUNT_FILE_ALT = path.resolve(__dirname, 'gce.alt.json');
+const GCE_SERVICE_ACCOUNT_FILE_INVALID = path.resolve(__dirname, 'gce.invalid.json');
+const GCE_SERVICE_ACCOUNT_FILE_MISSING = path.resolve(__dirname, 'gce.missing.json');
+
+let credentials;
+
+module.exports = {
+ before: (client, done) => {
+ credentials = client.page.credentials();
+
+ client.login();
+ client.waitForAngular();
+
+ credentials.section.navigation
+ .waitForElementVisible('@credentials')
+ .click('@credentials');
+
+ credentials
+ .waitForElementVisible('div.spinny')
+ .waitForElementNotVisible('div.spinny');
+
+ credentials.section.list
+ .waitForElementVisible('@add')
+ .click('@add');
+
+ credentials.section.add.section.details
+ .waitForElementVisible('@save')
+ .setValue('@name', `credential-${uuid().substr(0, 8)}`)
+ .setValue('@type', 'Google Compute Engine', done);
+ },
+ 'expected fields are initially visible and enabled': client => {
+ const { details } = credentials.section.add.section;
+ const { gce } = details.section;
+
+ details.expect.element('@name').visible;
+ details.expect.element('@description').visible;
+ details.expect.element('@organization').visible;
+ details.expect.element('@type').visible;
+
+ gce.expect.element('@email').visible;
+ gce.expect.element('@sshKeyData').visible;
+ gce.expect.element('@project').visible;
+ gce.expect.element('@serviceAccountFile').visible;
+
+ details.expect.element('@name').enabled;
+ details.expect.element('@description').enabled;
+ details.expect.element('@organization').enabled;
+ details.expect.element('@type').enabled;
+
+ gce.expect.element('@email').enabled;
+ gce.expect.element('@sshKeyData').enabled;
+ gce.expect.element('@project').enabled;
+ gce.expect.element('@serviceAccountFile').enabled;
+
+ gce.section.serviceAccountFile.expect.element('form i[class*="folder"]').visible;
+ gce.section.serviceAccountFile.expect.element('form i[class*="folder"]').enabled;
+ gce.section.serviceAccountFile.expect.element('form i[class*="trash"]').not.present;
+ },
+ 'select valid credential file': client => {
+ const { details } = credentials.section.add.section;
+ const { gce } = details.section;
+
+ gce.section.serviceAccountFile.setValue('form input[type="file"]', GCE_SERVICE_ACCOUNT_FILE);
+
+ gce.expect.element('@email').not.enabled;
+ gce.expect.element('@sshKeyData').not.enabled;
+ gce.expect.element('@project').not.enabled;
+ gce.expect.element('@serviceAccountFile').enabled;
+
+ gce.section.serviceAccountFile.expect.element('form i[class*="trash"]').visible;
+ gce.section.serviceAccountFile.expect.element('form i[class*="trash"]').enabled;
+ gce.section.serviceAccountFile.expect.element('form i[class*="folder"]').not.present;
+ },
+ 'deselect valid credential file': client => {
+ const { details } = credentials.section.add.section;
+ const { gce } = details.section;
+
+ gce.section.serviceAccountFile.click('form i[class*="trash"]');
+
+ gce.expect.element('@email').enabled;
+ gce.expect.element('@sshKeyData').enabled;
+ gce.expect.element('@project').enabled;
+ gce.expect.element('@serviceAccountFile').enabled;
+
+ gce.section.serviceAccountFile.expect.element('form i[class*="folder"]').visible;
+ gce.section.serviceAccountFile.expect.element('form i[class*="folder"]').enabled;
+ gce.section.serviceAccountFile.expect.element('form i[class*="trash"]').not.present;
+
+ gce.section.email.expect.element('@error').visible;
+ gce.section.sshKeyData.expect.element('@error').visible;
+
+ gce.section.project.expect.element('@error').not.present;
+ gce.section.serviceAccountFile.expect.element('@error').not.present;
+ },
+ 'select credential file with missing field': client => {
+ const { details } = credentials.section.add.section;
+ const { gce } = details.section;
+
+ gce.section.serviceAccountFile.setValue('form input[type="file"]', GCE_SERVICE_ACCOUNT_FILE_MISSING);
+
+ gce.expect.element('@email').not.enabled;
+ gce.expect.element('@sshKeyData').not.enabled;
+ gce.expect.element('@project').not.enabled;
+ gce.expect.element('@serviceAccountFile').enabled;
+
+ gce.section.serviceAccountFile.expect.element('form i[class*="trash"]').visible;
+ gce.section.serviceAccountFile.expect.element('form i[class*="trash"]').enabled;
+ gce.section.serviceAccountFile.expect.element('form i[class*="folder"]').not.present;
+
+ gce.section.email.expect.element('@error').visible;
+
+ gce.section.project.expect.element('@error').not.present;
+ gce.section.serviceAccountFile.expect.element('@error').not.present;
+ gce.section.sshKeyData.expect.element('@error').not.present;
+ },
+ 'deselect credential file with missing field': client => {
+ const { details } = credentials.section.add.section;
+ const { gce } = details.section;
+
+ gce.section.serviceAccountFile.click('form i[class*="trash"]');
+
+ gce.expect.element('@email').enabled;
+ gce.expect.element('@sshKeyData').enabled;
+ gce.expect.element('@project').enabled;
+ gce.expect.element('@serviceAccountFile').enabled;
+
+ gce.section.serviceAccountFile.expect.element('form i[class*="folder"]').visible;
+ gce.section.serviceAccountFile.expect.element('form i[class*="folder"]').enabled;
+ gce.section.serviceAccountFile.expect.element('form i[class*="trash"]').not.present;
+
+ gce.section.email.expect.element('@error').visible;
+ gce.section.sshKeyData.expect.element('@error').visible;
+
+ gce.section.project.expect.element('@error').not.present;
+ gce.section.serviceAccountFile.expect.element('@error').not.present;
+ },
+ 'select invalid credential file': client => {
+ const { details } = credentials.section.add.section;
+ const { gce } = details.section;
+
+ gce.section.serviceAccountFile.setValue('form input[type="file"]', GCE_SERVICE_ACCOUNT_FILE_INVALID);
+
+ gce.expect.element('@email').not.enabled;
+ gce.expect.element('@sshKeyData').not.enabled;
+ gce.expect.element('@project').not.enabled;
+ gce.expect.element('@serviceAccountFile').enabled;
+
+ gce.section.serviceAccountFile.expect.element('form i[class*="trash"]').visible;
+ gce.section.serviceAccountFile.expect.element('form i[class*="trash"]').enabled;
+ gce.section.serviceAccountFile.expect.element('form i[class*="folder"]').not.present;
+
+ gce.section.email.expect.element('@error').visible;
+ gce.section.serviceAccountFile.expect.element('@error').visible;
+ gce.section.sshKeyData.expect.element('@error').visible;
+
+ gce.section.project.expect.element('@error').not.present;
+ },
+ 'deselect invalid credential file': client => {
+ const { details } = credentials.section.add.section;
+ const { gce } = details.section;
+
+ gce.section.serviceAccountFile.click('form i[class*="trash"]');
+
+ gce.expect.element('@email').enabled;
+ gce.expect.element('@sshKeyData').enabled;
+ gce.expect.element('@project').enabled;
+ gce.expect.element('@serviceAccountFile').enabled;
+
+ gce.section.serviceAccountFile.expect.element('form i[class*="folder"]').visible;
+ gce.section.serviceAccountFile.expect.element('form i[class*="folder"]').enabled;
+ gce.section.serviceAccountFile.expect.element('form i[class*="trash"]').not.present;
+
+ gce.section.email.expect.element('@error').visible;
+ gce.section.sshKeyData.expect.element('@error').visible;
+
+ gce.section.project.expect.element('@error').not.present;
+ gce.section.serviceAccountFile.expect.element('@error').not.present;
+ },
+ 'save valid credential file': client => {
+ const add = credentials.section.add.section.details;
+ const edit = credentials.section.edit.section.details;
+
+ add.section.gce.section.serviceAccountFile.setValue('form input[type="file"]', GCE_SERVICE_ACCOUNT_FILE);
+
+ add.section.gce.expect.element('@email').not.enabled;
+ add.section.gce.expect.element('@sshKeyData').not.enabled;
+ add.section.gce.expect.element('@project').not.enabled;
+ add.section.gce.expect.element('@serviceAccountFile').enabled;
+
+ add.section.gce.section.serviceAccountFile.expect.element('form i[class*="trash"]').visible;
+ add.section.gce.section.serviceAccountFile.expect.element('form i[class*="trash"]').enabled;
+ add.section.gce.section.serviceAccountFile.expect.element('form i[class*="folder"]').not.present;
+
+ add.click('@save');
+
+ credentials
+ .waitForElementVisible('div.spinny')
+ .waitForElementNotVisible('div.spinny');
+
+ edit.section.gce.expect.element('@email').enabled;
+ edit.section.gce.expect.element('@project').enabled;
+
+ edit.section.gce.expect.element('@serviceAccountFile').not.enabled;
+ edit.section.gce.expect.element('@sshKeyData').not.enabled;
+ },
+ 'select and deselect credential file when replacing private key': client => {
+ const { gce } = credentials.section.edit.section.details.section;
+
+ gce.section.sshKeyData.waitForElementVisible('@replace');
+ gce.section.sshKeyData.click('@replace');
+
+ gce.expect.element('@email').enabled;
+ gce.expect.element('@project').enabled;
+ gce.expect.element('@sshKeyData').enabled;
+ gce.expect.element('@serviceAccountFile').enabled;
+
+ gce.section.sshKeyData.expect.element('@error').visible;
+
+ gce.section.email.expect.element('@error').not.present;
+ gce.section.project.expect.element('@error').not.present;
+ gce.section.serviceAccountFile.expect.element('@error').not.present;
+
+ gce.section.serviceAccountFile.setValue('form input[type="file"]', GCE_SERVICE_ACCOUNT_FILE_ALT);
+
+ gce.expect.element('@serviceAccountFile').enabled;
+
+ gce.expect.element('@email').not.enabled;
+ gce.expect.element('@sshKeyData').not.enabled;
+ gce.expect.element('@project').not.enabled;
+
+ gce.section.email.expect.element('@error').not.present;
+ gce.section.project.expect.element('@error').not.present;
+ gce.section.sshKeyData.expect.element('@error').not.present;
+ gce.section.serviceAccountFile.expect.element('@error').not.present;
+
+ gce.section.sshKeyData.expect.element('@replace').not.present;
+ gce.section.sshKeyData.expect.element('@revert').present;
+ gce.section.sshKeyData.expect.element('@revert').not.enabled;
+
+ gce.section.serviceAccountFile.click('form i[class*="trash"]');
+
+ gce.expect.element('@email').enabled;
+ gce.expect.element('@sshKeyData').enabled;
+ gce.expect.element('@project').enabled;
+ gce.expect.element('@serviceAccountFile').enabled;
+
+ gce.section.sshKeyData.expect.element('@error').visible;
+
+ gce.section.email.expect.element('@error').not.present;
+ gce.section.project.expect.element('@error').not.present;
+ gce.section.serviceAccountFile.expect.element('@error').not.present;
+
+ gce.section.sshKeyData.expect.element('@revert').enabled;
+
+ gce.section.sshKeyData.click('@revert');
+
+ gce.expect.element('@email').enabled;
+ gce.expect.element('@project').enabled;
+
+ gce.expect.element('@serviceAccountFile').not.enabled;
+ gce.expect.element('@sshKeyData').not.enabled;
+
+ client.end();
+ }
+};
diff --git a/awx/ui/test/unit/components/file.unit.js b/awx/ui/test/unit/components/file.unit.js
new file mode 100644
index 0000000000..1bf7041b56
--- /dev/null
+++ b/awx/ui/test/unit/components/file.unit.js
@@ -0,0 +1,61 @@
+describe('Components | Input | File', () => {
+ let $scope;
+ let element;
+ let state;
+ let controller;
+
+ const getMockFileEvent = file => ({ target: { files: [file] } });
+
+ beforeEach(() => {
+ angular.mock.module('at.lib.services');
+ angular.mock.module('at.lib.components');
+ });
+
+ describe('AtInputFileController', () => {
+ beforeEach(angular.mock.inject(($rootScope, $compile) => {
+ const component = '';
+ const dom = angular.element(`${component}`);
+
+ $scope = $rootScope.$new();
+ $scope.vm = { form: { disabled: false, unit: {} } };
+
+ $compile(dom)($scope);
+ $scope.$digest();
+
+ element = dom.find('#unit');
+ state = $scope.vm.form.unit;
+ controller = element.controller('atInputFile');
+ }));
+
+ it('should initialize without a value by default', () => {
+ expect(state._value).not.toBeDefined();
+ expect(state._displayValue).not.toBeDefined();
+ });
+
+ it('should update display value with file name when file is read', () => {
+ const name = 'notavirus.exe';
+ const reader = { result: 'AAAAAAA' };
+
+ controller.check = jasmine.createSpy('check');
+
+ controller.readFile(reader, getMockFileEvent({ name }));
+
+ $scope.$digest();
+
+ expect(state._value).toBeDefined();
+ expect(state._displayValue).toEqual(name);
+
+ expect(controller.check).toHaveBeenCalled();
+ });
+
+ it('should notify handler on file input change event', () => {
+ controller.handleFileChangeEvent = jasmine.createSpy('handleFileChangeEvent');
+
+ element.find('input')[0].dispatchEvent(new Event('change'));
+
+ $scope.$digest();
+
+ expect(controller.handleFileChangeEvent).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/awx/ui/test/unit/components/index.js b/awx/ui/test/unit/components/index.js
index 398445bc6f..8d75e3cf71 100644
--- a/awx/ui/test/unit/components/index.js
+++ b/awx/ui/test/unit/components/index.js
@@ -2,6 +2,7 @@
import 'angular-mocks';
// Import tests
+import './file.unit';
import './layout.unit';
import './side-nav.unit';
import './side-nav-item.unit';