Allow username editing if permitted by project cfg

Change-Id: I57986af606833c20535c719a1f7540d22d4602bb
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html
index 494b9e8..5dbd466 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html
@@ -44,17 +44,28 @@
               date-str="[[_account.registered_on]]"></gr-date-formatter>
         </span>
       </section>
-      <section>
+      <section id="usernameSection">
         <span class="title">Username</span>
-        <span class="value">[[_account.username]]</span>
+        <span
+            hidden$="[[usernameMutable]]"
+            class="value">[[_account.username]]</span>
+        <span
+            hidden$="[[!usernameMutable]]"
+            class="value">
+          <input
+              is="iron-input"
+              id="usernameInput"
+              disabled="[[_saving]]"
+              on-keydown="_handleKeydown"
+              bind-value="{{_account.username}}">
       </section>
       <section id="nameSection">
         <span class="title">Full name</span>
         <span
-            hidden$="[[mutable]]"
+            hidden$="[[nameMutable]]"
             class="value">[[_account.name]]</span>
         <span
-            hidden$="[[!mutable]]"
+            hidden$="[[!nameMutable]]"
             class="value">
           <input
               is="iron-input"
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
index 5d7f8a6..03795f6 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
@@ -24,21 +24,31 @@
      */
 
     properties: {
-      mutable: {
+      usernameMutable: {
         type: Boolean,
         notify: true,
-        computed: '_computeMutable(_serverConfig)',
+        computed: '_computeUsernameMutable(_serverConfig)',
+      },
+      nameMutable: {
+        type: Boolean,
+        notify: true,
+        computed: '_computeNameMutable(_serverConfig)',
       },
       hasUnsavedChanges: {
         type: Boolean,
         notify: true,
-        computed: '_computeHasUnsavedChanges(_hasNameChange, _hasStatusChange)',
+        computed: '_computeHasUnsavedChanges(_hasNameChange, ' +
+            '_hasUsernameChange, _hasStatusChange)',
       },
 
       _hasNameChange: {
         type: Boolean,
         value: false,
       },
+      _hasUsernameChange: {
+        type: Boolean,
+        value: false,
+      },
       _hasStatusChange: {
         type: Boolean,
         value: false,
@@ -58,6 +68,7 @@
 
     observers: [
       '_nameChanged(_account.name)',
+      '_usernameChanged(_account.username)',
       '_statusChanged(_account.status)',
     ],
 
@@ -88,6 +99,7 @@
       // Set only the fields that have changed.
       // Must be done in sequence to avoid race conditions (@see Issue 5721)
       return this._maybeSetName()
+          .then(this._maybeSetUsername.bind(this))
           .then(this._maybeSetStatus.bind(this))
           .then(() => {
             this._hasNameChange = false;
@@ -98,9 +110,15 @@
     },
 
     _maybeSetName() {
-      return this._hasNameChange && this.mutable ?
-                this.$.restAPI.setAccountName(this._account.name) :
-                Promise.resolve();
+      return this._hasNameChange && this.nameMutable ?
+          this.$.restAPI.setAccountName(this._account.name) :
+          Promise.resolve();
+    },
+
+    _maybeSetUsername() {
+      return this._hasUsernameChange && this.usernameMutable ?
+          this.$.restAPI.setAccountUsername(this._account.username) :
+          Promise.resolve();
     },
 
     _maybeSetStatus() {
@@ -109,11 +127,15 @@
           Promise.resolve();
     },
 
-    _computeHasUnsavedChanges(name, status) {
-      return name || status;
+    _computeHasUnsavedChanges(nameChanged, usernameChanged, statusChanged) {
+      return nameChanged || usernameChanged || statusChanged;
     },
 
-    _computeMutable(config) {
+    _computeUsernameMutable(config) {
+      return config.auth.editable_account_fields.includes('USER_NAME');
+    },
+
+    _computeNameMutable(config) {
       return config.auth.editable_account_fields.includes('FULL_NAME');
     },
 
@@ -122,6 +144,11 @@
       this._hasStatusChange = true;
     },
 
+    _usernameChanged() {
+      if (this._loading) { return; }
+      this._hasUsernameChange = true;
+    },
+
     _nameChanged() {
       if (this._loading) { return; }
       this._hasNameChange = true;
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
index 84fbc09..e46be5b 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
@@ -84,18 +84,18 @@
       assert.equal(valueOf('Username').textContent, account.username);
     });
 
-    test('user name render (immutable)', () => {
+    test('full name render (immutable)', () => {
       const section = element.$.nameSection;
       const displaySpan = section.querySelectorAll('.value')[0];
       const inputSpan = section.querySelectorAll('.value')[1];
 
-      assert.isFalse(element.mutable);
+      assert.isFalse(element.nameMutable);
       assert.isFalse(displaySpan.hasAttribute('hidden'));
       assert.equal(displaySpan.textContent, account.name);
       assert.isTrue(inputSpan.hasAttribute('hidden'));
     });
 
-    test('user name render (mutable)', () => {
+    test('full name render (mutable)', () => {
       element.set('_serverConfig',
           {auth: {editable_account_fields: ['FULL_NAME']}});
 
@@ -103,32 +103,62 @@
       const displaySpan = section.querySelectorAll('.value')[0];
       const inputSpan = section.querySelectorAll('.value')[1];
 
-      assert.isTrue(element.mutable);
+      assert.isTrue(element.nameMutable);
       assert.isTrue(displaySpan.hasAttribute('hidden'));
       assert.equal(element.$.nameInput.bindValue, account.name);
       assert.isFalse(inputSpan.hasAttribute('hidden'));
     });
 
+    test('username render (immutable)', () => {
+      const section = element.$.usernameSection;
+      const displaySpan = section.querySelectorAll('.value')[0];
+      const inputSpan = section.querySelectorAll('.value')[1];
+
+      assert.isFalse(element.usernameMutable);
+      assert.isFalse(displaySpan.hasAttribute('hidden'));
+      assert.equal(displaySpan.textContent, account.username);
+      assert.isTrue(inputSpan.hasAttribute('hidden'));
+    });
+
+    test('username render (mutable)', () => {
+      element.set('_serverConfig',
+          {auth: {editable_account_fields: ['USER_NAME']}});
+
+      const section = element.$.usernameSection;
+      const displaySpan = section.querySelectorAll('.value')[0];
+      const inputSpan = section.querySelectorAll('.value')[1];
+
+      assert.isTrue(element.usernameMutable);
+      assert.isTrue(displaySpan.hasAttribute('hidden'));
+      assert.equal(element.$.usernameInput.bindValue, account.username);
+      assert.isFalse(inputSpan.hasAttribute('hidden'));
+    });
+
     suite('account info edit', () => {
       let nameChangedSpy;
+      let usernameChangedSpy;
       let statusChangedSpy;
       let nameStub;
+      let usernameStub;
       let statusStub;
 
       setup(() => {
         nameChangedSpy = sandbox.spy(element, '_nameChanged');
+        usernameChangedSpy = sandbox.spy(element, '_usernameChanged');
         statusChangedSpy = sandbox.spy(element, '_statusChanged');
         element.set('_serverConfig',
-          {auth: {editable_account_fields: ['FULL_NAME']}});
+          {auth: {editable_account_fields: ['FULL_NAME', 'USER_NAME']}});
 
-        nameStub = sandbox.stub(element.$.restAPI, 'setAccountName', name =>
-          Promise.resolve());
+        nameStub = sandbox.stub(element.$.restAPI, 'setAccountName',
+            name => Promise.resolve());
+        usernameStub = sandbox.stub(element.$.restAPI, 'setAccountUsername',
+            username => Promise.resolve());
         statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus',
             status => Promise.resolve());
       });
 
       test('name', done => {
-        assert.isTrue(element.mutable);
+        assert.isTrue(element.nameMutable);
         assert.isFalse(element.hasUnsavedChanges);
 
         element.set('_account.name', 'new name');
@@ -137,18 +167,39 @@
         assert.isFalse(statusChangedSpy.called);
         assert.isTrue(element.hasUnsavedChanges);
 
-        MockInteractions.pressAndReleaseKeyOn(element.$.nameInput, 13);
+        element.save().then(() => {
+          assert.isFalse(usernameStub.called);
+          assert.isTrue(nameStub.called);
+          assert.isFalse(statusStub.called);
+          nameStub.lastCall.returnValue.then(() => {
+            assert.equal(nameStub.lastCall.args[0], 'new name');
+            done();
+          });
+        });
+      });
 
-        assert.isTrue(nameStub.called);
-        assert.isFalse(statusStub.called);
-        nameStub.lastCall.returnValue.then(() => {
-          assert.equal(nameStub.lastCall.args[0], 'new name');
-          done();
+      test('username', done => {
+        assert.isTrue(element.usernameMutable);
+        assert.isFalse(element.hasUnsavedChanges);
+
+        element.set('_account.username', 'new username');
+
+        assert.isTrue(usernameChangedSpy.called);
+        assert.isFalse(statusChangedSpy.called);
+        assert.isTrue(element.hasUnsavedChanges);
+
+        element.save().then(() => {
+          assert.isTrue(usernameStub.called);
+          assert.isFalse(nameStub.called);
+          assert.isFalse(statusStub.called);
+          usernameStub.lastCall.returnValue.then(() => {
+            assert.equal(usernameStub.lastCall.args[0], 'new username');
+            done();
+          });
         });
       });
 
       test('status', done => {
-        assert.isTrue(element.mutable);
         assert.isFalse(element.hasUnsavedChanges);
 
         element.set('_account.status', 'new status');
@@ -158,6 +209,7 @@
         assert.isTrue(element.hasUnsavedChanges);
 
         element.save().then(() => {
+          assert.isFalse(usernameStub.called);
           assert.isTrue(statusStub.called);
           assert.isFalse(nameStub.called);
           statusStub.lastCall.returnValue.then(() => {
@@ -178,16 +230,18 @@
         nameChangedSpy = sandbox.spy(element, '_nameChanged');
         statusChangedSpy = sandbox.spy(element, '_statusChanged');
         element.set('_serverConfig',
-          {auth: {editable_account_fields: ['FULL_NAME']}});
+            {auth: {editable_account_fields: ['FULL_NAME']}});
 
-        nameStub = sandbox.stub(element.$.restAPI, 'setAccountName', name =>
-          Promise.resolve());
+        nameStub = sandbox.stub(element.$.restAPI, 'setAccountName',
+            name => Promise.resolve());
         statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus',
             status => Promise.resolve());
+        sandbox.stub(element.$.restAPI, 'setAccountUsername',
+            username => Promise.resolve());
       });
 
       test('set name and status', done => {
-        assert.isTrue(element.mutable);
+        assert.isTrue(element.nameMutable);
         assert.isFalse(element.hasUnsavedChanges);
 
         element.set('_account.name', 'new name');
@@ -231,7 +285,7 @@
         const displaySpan = section.querySelectorAll('.value')[0];
         const inputSpan = section.querySelectorAll('.value')[1];
 
-        assert.isFalse(element.mutable);
+        assert.isFalse(element.nameMutable);
 
         assert.isFalse(element.hasUnsavedChanges);