Please record this token somewhere, you cannot retrieve
its value again. For use on the command line you can save it to
~/.cargo/credentials
diff --git a/app/routes/login.js b/app/routes/login.js
index f4f15387120..f882cdca256 100644
--- a/app/routes/login.js
+++ b/app/routes/login.js
@@ -15,7 +15,7 @@ export default Route.extend({
beforeModel(transition) {
try {
- localStorage.removeItem('github_response');
+ window.localStorage.removeItem('github_response');
} catch (e) {
// ignore error
}
diff --git a/app/services/session.js b/app/services/session.js
index f37f5c47fc9..498c8535739 100644
--- a/app/services/session.js
+++ b/app/services/session.js
@@ -1,6 +1,7 @@
import { A } from '@ember/array';
import Service, { inject as service } from '@ember/service';
import ajax from 'ember-fetch/ajax';
+import window from 'ember-window-mock';
export default Service.extend({
savedTransition: null,
@@ -17,7 +18,7 @@ export default Service.extend({
this._super(...arguments);
let isLoggedIn;
try {
- isLoggedIn = localStorage.getItem('isLoggedIn') === '1';
+ isLoggedIn = window.localStorage.getItem('isLoggedIn') === '1';
} catch (e) {
isLoggedIn = false;
}
@@ -29,7 +30,7 @@ export default Service.extend({
this.set('isLoggedIn', true);
this.set('currentUser', user);
try {
- localStorage.setItem('isLoggedIn', '1');
+ window.localStorage.setItem('isLoggedIn', '1');
} catch (e) {
// ignore error
}
@@ -42,7 +43,7 @@ export default Service.extend({
this.set('currentUser', null);
try {
- localStorage.removeItem('isLoggedIn');
+ window.localStorage.removeItem('isLoggedIn');
} catch (e) {
// ignore error
}
diff --git a/app/templates/me/index.hbs b/app/templates/me/index.hbs
index 83f43661cbe..4b036e4cd05 100644
--- a/app/templates/me/index.hbs
+++ b/app/templates/me/index.hbs
@@ -72,7 +72,15 @@
API Access
-
+
diff --git a/tests/acceptance/api-token-test.js b/tests/acceptance/api-token-test.js
new file mode 100644
index 00000000000..43cf656c89c
--- /dev/null
+++ b/tests/acceptance/api-token-test.js
@@ -0,0 +1,154 @@
+import { module, test } from 'qunit';
+import { setupApplicationTest } from 'ember-qunit';
+import { currentURL, findAll, click, fillIn } from '@ember/test-helpers';
+import window, { setupWindowMock } from 'ember-window-mock';
+import { Response } from 'ember-cli-mirage';
+
+import setupMirage from '../helpers/setup-mirage';
+import { visit } from '../helpers/visit-ignoring-abort';
+
+module('Acceptance | api-tokens', function(hooks) {
+ setupApplicationTest(hooks);
+ setupWindowMock(hooks);
+ setupMirage(hooks);
+
+ function prepare(context) {
+ window.localStorage.setItem('isLoggedIn', '1');
+
+ context.server.get('/api/v1/me', {
+ user: {
+ id: 42,
+ login: 'johnnydee',
+ email_verified: true,
+ email_verification_sent: true,
+ name: 'John Doe',
+ email: 'john@doe.com',
+ avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4',
+ url: 'https://github.com/johnnydee',
+ },
+ owned_crates: [],
+ });
+
+ context.server.get('/api/v1/me/tokens', {
+ api_tokens: [
+ { id: 2, name: 'BAR', created_at: new Date('2017-11-19T17:59:22').toISOString(), last_used_at: null },
+ {
+ id: 1,
+ name: 'foo',
+ created_at: new Date('2017-08-01T12:34:56').toISOString(),
+ last_used_at: new Date('2017-11-02T01:45:14').toISOString(),
+ },
+ ],
+ });
+ }
+
+ test('/me is showing the list of active API tokens', async function(assert) {
+ prepare(this);
+
+ await visit('/me');
+ assert.equal(currentURL(), '/me');
+ assert.dom('[data-test-api-token]').exists({ count: 2 });
+
+ let [row1, row2] = findAll('[data-test-api-token]');
+ assert.dom('[data-test-name]', row1).hasText('BAR');
+ assert.dom('[data-test-created-at]', row1).hasText('Created 18 hours ago');
+ assert.dom('[data-test-last-used-at]', row1).hasText('Never used');
+ assert.dom('[data-test-save-token-button]', row1).doesNotExist();
+ assert.dom('[data-test-revoke-token-button]', row1).exists();
+ assert.dom('[data-test-saving-spinner]', row1).doesNotExist();
+ assert.dom('[data-test-error]', row1).doesNotExist();
+ assert.dom('[data-test-token]', row1).doesNotExist();
+
+ assert.dom('[data-test-name]', row2).hasText('foo');
+ assert.dom('[data-test-created-at]', row2).hasText('Created 4 months ago');
+ assert.dom('[data-test-last-used-at]', row2).hasText('Last used 18 days ago');
+ assert.dom('[data-test-save-token-button]', row2).doesNotExist();
+ assert.dom('[data-test-revoke-token-button]', row2).exists();
+ assert.dom('[data-test-saving-spinner]', row2).doesNotExist();
+ assert.dom('[data-test-error]', row2).doesNotExist();
+ assert.dom('[data-test-token]', row2).doesNotExist();
+ });
+
+ test('API tokens can be revoked', async function(assert) {
+ prepare(this);
+
+ this.server.delete('/api/v1/me/tokens/:id', function(schema, request) {
+ assert.step(`delete id:${request.params.id}`);
+ return {};
+ });
+
+ await visit('/me');
+ assert.equal(currentURL(), '/me');
+ assert.dom('[data-test-api-token]').exists({ count: 2 });
+
+ await click('[data-test-api-token="1"] [data-test-revoke-token-button]');
+ assert.verifySteps(['delete id:1']);
+
+ assert.dom('[data-test-api-token]').exists({ count: 1 });
+ assert.dom('[data-test-api-token="2"]').exists();
+ assert.dom('[data-test-error]').doesNotExist();
+ });
+
+ test('failed API tokens revocation shows an error', async function(assert) {
+ prepare(this);
+
+ this.server.delete('/api/v1/me/tokens/:id', function() {
+ return new Response(500, {}, {});
+ });
+
+ await visit('/me');
+ assert.equal(currentURL(), '/me');
+ assert.dom('[data-test-api-token]').exists({ count: 2 });
+
+ await click('[data-test-api-token="1"] [data-test-revoke-token-button]');
+ assert.dom('[data-test-api-token]').exists({ count: 2 });
+ assert.dom('[data-test-api-token="2"]').exists();
+ assert.dom('[data-test-api-token="1"]').exists();
+ assert.dom('[data-test-error]').includesText('An error occurred while revoking this token');
+ });
+
+ test('new API tokens can be created', async function(assert) {
+ prepare(this);
+
+ this.server.put('/api/v1/me/tokens', function(schema, request) {
+ assert.step('put');
+
+ let { api_token } = JSON.parse(request.requestBody);
+
+ return {
+ api_token: {
+ id: 5,
+ name: api_token.name,
+ token: 'zuz6nYcXJOzPDvnA9vucNwccG0lFSGbh',
+ revoked: false,
+ created_at: api_token.created_at,
+ last_used_at: api_token.last_used_at,
+ },
+ };
+ });
+
+ await visit('/me');
+ assert.equal(currentURL(), '/me');
+ assert.dom('[data-test-api-token]').exists({ count: 2 });
+ assert.dom('[data-test-focused-input]').doesNotExist();
+ assert.dom('[data-test-save-token-button]').doesNotExist();
+
+ await click('[data-test-new-token-button]');
+ assert.dom('[data-test-new-token-button]').isDisabled();
+ assert.dom('[data-test-focused-input]').exists();
+ assert.dom('[data-test-save-token-button]').exists();
+
+ await fillIn('[data-test-focused-input]', 'the new token');
+ await click('[data-test-save-token-button]');
+ assert.verifySteps(['put']);
+ assert.dom('[data-test-focused-input]').doesNotExist();
+ assert.dom('[data-test-save-token-button]').doesNotExist();
+
+ assert.dom('[data-test-api-token="5"] [data-test-name]').hasText('the new token');
+ assert.dom('[data-test-api-token="5"] [data-test-save-token-button]').doesNotExist();
+ assert.dom('[data-test-api-token="5"] [data-test-revoke-token-button]').exists();
+ assert.dom('[data-test-api-token="5"] [data-test-saving-spinner]').doesNotExist();
+ assert.dom('[data-test-api-token="5"] [data-test-error]').doesNotExist();
+ assert.dom('[data-test-token]').includesText('cargo login zuz6nYcXJOzPDvnA9vucNwccG0lFSGbh');
+ });
+});
diff --git a/tests/helpers/visit-ignoring-abort.js b/tests/helpers/visit-ignoring-abort.js
new file mode 100644
index 00000000000..3e5d366b7d3
--- /dev/null
+++ b/tests/helpers/visit-ignoring-abort.js
@@ -0,0 +1,14 @@
+import { settled, visit as _visit } from '@ember/test-helpers';
+
+// see https://github.com/emberjs/ember-test-helpers/issues/332
+export async function visit(url) {
+ try {
+ await _visit(url);
+ } catch (error) {
+ if (error.message !== 'TransitionAborted') {
+ throw error;
+ }
+ }
+
+ await settled();
+}