Skip to content

Commit 76aa8f7

Browse files
committed
mirage: Add generic /api/v1/me/token route handlers
1 parent 627ac6d commit 76aa8f7

File tree

6 files changed

+241
-42
lines changed

6 files changed

+241
-42
lines changed

mirage/factories/api-token.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Factory } from 'ember-cli-mirage';
2+
3+
export default Factory.extend({
4+
createdAt: '2017-11-19T17:59:22',
5+
lastUsedAt: null,
6+
name: i => `API Token ${i + 1}`,
7+
token: () => generateToken(),
8+
9+
afterCreate(model) {
10+
if (!model.user) {
11+
throw new Error('Missing `user` relationship on `api-token`');
12+
}
13+
},
14+
});
15+
16+
function generateToken() {
17+
return Math.random().toString().slice(2);
18+
}

mirage/models/api-token.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1-
import { Model } from 'ember-cli-mirage';
1+
import { Model, belongsTo } from 'ember-cli-mirage';
22

3-
export default Model.extend({});
3+
export default Model.extend({
4+
user: belongsTo(),
5+
});

mirage/route-handlers/me.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,43 @@ export function register(server) {
2222
return json;
2323
});
2424

25+
server.get('/api/v1/me/tokens', function (schema) {
26+
let { user } = getSession(schema);
27+
if (!user) {
28+
return new Response(403, {}, { errors: [{ detail: 'must be logged in to perform that action' }] });
29+
}
30+
31+
return schema.apiTokens.where({ userId: user.id }).sort((a, b) => Number(b.id) - Number(a.id));
32+
});
33+
34+
server.put('/api/v1/me/tokens', function (schema) {
35+
let { user } = getSession(schema);
36+
if (!user) {
37+
return new Response(403, {}, { errors: [{ detail: 'must be logged in to perform that action' }] });
38+
}
39+
40+
let { name } = this.normalizedRequestAttrs('api-token');
41+
let token = server.create('api-token', { user, name, createdAt: new Date().toISOString() });
42+
43+
let json = this.serialize(token);
44+
json.api_token.revoked = false;
45+
json.api_token.token = token.token;
46+
return json;
47+
});
48+
49+
server.delete('/api/v1/me/tokens/:tokenId', function (schema, request) {
50+
let { user } = getSession(schema);
51+
if (!user) {
52+
return new Response(403, {}, { errors: [{ detail: 'must be logged in to perform that action' }] });
53+
}
54+
55+
let { tokenId } = request.params;
56+
let token = schema.apiTokens.findBy({ id: tokenId, userId: user.id });
57+
if (token) token.destroy();
58+
59+
return {};
60+
});
61+
2562
server.put('/api/v1/confirm/:token', (schema, request) => {
2663
let { token } = request.params;
2764

mirage/serializers/api-token.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import BaseSerializer from './application';
2+
3+
export default BaseSerializer.extend({
4+
getHashForResource() {
5+
let [hash, addToIncludes] = BaseSerializer.prototype.getHashForResource.apply(this, arguments);
6+
7+
if (Array.isArray(hash)) {
8+
for (let resource of hash) {
9+
this._adjust(resource);
10+
}
11+
} else {
12+
this._adjust(hash);
13+
}
14+
15+
return [hash, addToIncludes];
16+
},
17+
18+
_adjust(hash) {
19+
hash.id = Number(hash.id);
20+
if (hash.created_at) {
21+
hash.created_at = new Date(hash.created_at).toISOString();
22+
}
23+
if (hash.last_used_at) {
24+
hash.last_used_at = new Date(hash.last_used_at).toISOString();
25+
}
26+
delete hash.token;
27+
delete hash.user_id;
28+
},
29+
});

tests/acceptance/api-token-test.js

Lines changed: 23 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,18 @@ module('Acceptance | api-tokens', function (hooks) {
1919
avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4',
2020
});
2121

22-
context.server.get('/api/v1/me/tokens', {
23-
api_tokens: [
24-
{ id: 2, name: 'BAR', created_at: new Date('2017-11-19T17:59:22').toISOString(), last_used_at: null },
25-
{
26-
id: 1,
27-
name: 'foo',
28-
created_at: new Date('2017-08-01T12:34:56').toISOString(),
29-
last_used_at: new Date('2017-11-02T01:45:14').toISOString(),
30-
},
31-
],
22+
context.server.create('api-token', {
23+
user,
24+
name: 'foo',
25+
createdAt: '2017-08-01T12:34:56',
26+
lastUsedAt: '2017-11-02T01:45:14',
27+
});
28+
29+
context.server.create('api-token', {
30+
user,
31+
name: 'BAR',
32+
createdAt: '2017-11-19T17:59:22',
33+
lastUsedAt: null,
3234
});
3335

3436
context.authenticateAs(user);
@@ -64,17 +66,12 @@ module('Acceptance | api-tokens', function (hooks) {
6466
test('API tokens can be revoked', async function (assert) {
6567
prepare(this);
6668

67-
this.server.delete('/api/v1/me/tokens/:id', function (schema, request) {
68-
assert.step(`delete id:${request.params.id}`);
69-
return {};
70-
});
71-
7269
await visit('/me');
7370
assert.equal(currentURL(), '/me');
7471
assert.dom('[data-test-api-token]').exists({ count: 2 });
7572

7673
await click('[data-test-api-token="1"] [data-test-revoke-token-button]');
77-
assert.verifySteps(['delete id:1']);
74+
assert.equal(this.server.schema.apiTokens.all().length, 1, 'API token has been deleted from the backend database');
7875

7976
assert.dom('[data-test-api-token]').exists({ count: 1 });
8077
assert.dom('[data-test-api-token="2"]').exists();
@@ -102,23 +99,6 @@ module('Acceptance | api-tokens', function (hooks) {
10299
test('new API tokens can be created', async function (assert) {
103100
prepare(this);
104101

105-
this.server.put('/api/v1/me/tokens', function (schema, request) {
106-
assert.step('put');
107-
108-
let { api_token } = JSON.parse(request.requestBody);
109-
110-
return {
111-
api_token: {
112-
id: 5,
113-
name: api_token.name,
114-
token: 'zuz6nYcXJOzPDvnA9vucNwccG0lFSGbh',
115-
revoked: false,
116-
created_at: api_token.created_at,
117-
last_used_at: api_token.last_used_at,
118-
},
119-
};
120-
});
121-
122102
await visit('/me');
123103
assert.equal(currentURL(), '/me');
124104
assert.dom('[data-test-api-token]').exists({ count: 2 });
@@ -134,15 +114,18 @@ module('Acceptance | api-tokens', function (hooks) {
134114
percySnapshot(assert);
135115

136116
await click('[data-test-save-token-button]');
137-
assert.verifySteps(['put']);
117+
118+
let token = this.server.schema.apiTokens.findBy({ name: 'the new token' });
119+
assert.ok(Boolean(token), 'API token has been created in the backend database');
120+
138121
assert.dom('[data-test-focused-input]').doesNotExist();
139122
assert.dom('[data-test-save-token-button]').doesNotExist();
140123

141-
assert.dom('[data-test-api-token="5"] [data-test-name]').hasText('the new token');
142-
assert.dom('[data-test-api-token="5"] [data-test-save-token-button]').doesNotExist();
143-
assert.dom('[data-test-api-token="5"] [data-test-revoke-token-button]').exists();
144-
assert.dom('[data-test-api-token="5"] [data-test-saving-spinner]').doesNotExist();
145-
assert.dom('[data-test-api-token="5"] [data-test-error]').doesNotExist();
146-
assert.dom('[data-test-token]').includesText('cargo login zuz6nYcXJOzPDvnA9vucNwccG0lFSGbh');
124+
assert.dom('[data-test-api-token="3"] [data-test-name]').hasText('the new token');
125+
assert.dom('[data-test-api-token="3"] [data-test-save-token-button]').doesNotExist();
126+
assert.dom('[data-test-api-token="3"] [data-test-revoke-token-button]').exists();
127+
assert.dom('[data-test-api-token="3"] [data-test-saving-spinner]').doesNotExist();
128+
assert.dom('[data-test-api-token="3"] [data-test-error]').doesNotExist();
129+
assert.dom('[data-test-token]').includesText(`cargo login ${token.token}`);
147130
});
148131
});

tests/mirage/me-test.js

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { module, test } from 'qunit';
33

44
import setupMirage from '../helpers/setup-mirage';
55
import fetch from 'fetch';
6+
import timekeeper from 'timekeeper';
67

78
module('Mirage | /me', function (hooks) {
89
setupTest(hooks);
@@ -64,6 +65,135 @@ module('Mirage | /me', function (hooks) {
6465
});
6566
});
6667

68+
module('GET /api/v1/me/tokens', function () {
69+
test('returns the list of API token for the authenticated `user`', async function (assert) {
70+
let user = this.server.create('user');
71+
this.server.create('mirage-session', { user });
72+
73+
this.server.create('api-token', { user });
74+
this.server.create('api-token', { user });
75+
this.server.create('api-token', { user });
76+
77+
let response = await fetch('/api/v1/me/tokens');
78+
assert.equal(response.status, 200);
79+
80+
let responsePayload = await response.json();
81+
assert.deepEqual(responsePayload, {
82+
api_tokens: [
83+
{
84+
id: 3,
85+
created_at: '2017-11-19T16:59:22.000Z',
86+
last_used_at: null,
87+
name: 'API Token 3',
88+
},
89+
{
90+
id: 2,
91+
created_at: '2017-11-19T16:59:22.000Z',
92+
last_used_at: null,
93+
name: 'API Token 2',
94+
},
95+
{
96+
id: 1,
97+
created_at: '2017-11-19T16:59:22.000Z',
98+
last_used_at: null,
99+
name: 'API Token 1',
100+
},
101+
],
102+
});
103+
});
104+
105+
test('empty list case', async function (assert) {
106+
let user = this.server.create('user');
107+
this.server.create('mirage-session', { user });
108+
109+
let response = await fetch('/api/v1/me/tokens');
110+
assert.equal(response.status, 200);
111+
112+
let responsePayload = await response.json();
113+
assert.deepEqual(responsePayload, { api_tokens: [] });
114+
});
115+
116+
test('returns an error if unauthenticated', async function (assert) {
117+
let response = await fetch('/api/v1/me/tokens');
118+
assert.equal(response.status, 403);
119+
120+
let responsePayload = await response.json();
121+
assert.deepEqual(responsePayload, {
122+
errors: [{ detail: 'must be logged in to perform that action' }],
123+
});
124+
});
125+
});
126+
127+
module('PUT /api/v1/me/tokens', function () {
128+
test('creates a new API token', async function (assert) {
129+
timekeeper.freeze(new Date('2017-11-20T11:23:45Z'));
130+
131+
let user = this.server.create('user');
132+
this.server.create('mirage-session', { user });
133+
134+
let body = JSON.stringify({ api_token: { name: 'foooo' } });
135+
let response = await fetch('/api/v1/me/tokens', { method: 'PUT', body });
136+
assert.equal(response.status, 200);
137+
138+
let token = this.server.schema.apiTokens.all().models[0];
139+
assert.ok(token);
140+
141+
let responsePayload = await response.json();
142+
assert.deepEqual(responsePayload, {
143+
api_token: {
144+
id: 1,
145+
created_at: '2017-11-20T11:23:45.000Z',
146+
last_used_at: null,
147+
name: 'foooo',
148+
revoked: false,
149+
token: token.token,
150+
},
151+
});
152+
});
153+
154+
test('returns an error if unauthenticated', async function (assert) {
155+
let body = JSON.stringify({ api_token: {} });
156+
let response = await fetch('/api/v1/me/tokens', { method: 'PUT', body });
157+
assert.equal(response.status, 403);
158+
159+
let responsePayload = await response.json();
160+
assert.deepEqual(responsePayload, {
161+
errors: [{ detail: 'must be logged in to perform that action' }],
162+
});
163+
});
164+
});
165+
166+
module('DELETE /api/v1/me/tokens/:tokenId', function () {
167+
test('revokes an API token', async function (assert) {
168+
let user = this.server.create('user');
169+
this.server.create('mirage-session', { user });
170+
171+
let token = this.server.create('api-token', { user });
172+
173+
let response = await fetch(`/api/v1/me/tokens/${token.id}`, { method: 'DELETE' });
174+
assert.equal(response.status, 200);
175+
176+
let responsePayload = await response.json();
177+
assert.deepEqual(responsePayload, {});
178+
179+
let tokens = this.server.schema.apiTokens.all().models;
180+
assert.equal(tokens.length, 0);
181+
});
182+
183+
test('returns an error if unauthenticated', async function (assert) {
184+
let user = this.server.create('user');
185+
let token = this.server.create('api-token', { user });
186+
187+
let response = await fetch(`/api/v1/me/tokens/${token.id}`, { method: 'DELETE' });
188+
assert.equal(response.status, 403);
189+
190+
let responsePayload = await response.json();
191+
assert.deepEqual(responsePayload, {
192+
errors: [{ detail: 'must be logged in to perform that action' }],
193+
});
194+
});
195+
});
196+
67197
module('GET /api/v1/confirm/:token', function () {
68198
test('returns `ok: true` for a known token (unauthenticated)', async function (assert) {
69199
let user = this.server.create('user', { emailVerificationToken: 'foo' });

0 commit comments

Comments
 (0)