Skip to content

Commit 157bdb4

Browse files
committed
Auto merge of #2386 - Turbo87:mirage, r=locks
Improve Mirage setup This PR improves our "fake backend" setup which uses https://www.ember-cli-mirage.com/ Due to the number of changes this PR is likely best reviewed commit-by-commit. The main change is that this PR will make it easier to simulate the logged-in state of the application by providing a relatively simple `this.authenticateAs(user)` helper method on the test contexts. r? @locks
2 parents b9603d1 + a610bcf commit 157bdb4

26 files changed

+605
-197
lines changed

mirage/config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as Categories from './route-handlers/categories';
22
import * as Crates from './route-handlers/crates';
33
import * as DocsRS from './route-handlers/docs-rs';
44
import * as Keywords from './route-handlers/keywords';
5+
import * as Me from './route-handlers/me';
56
import * as Summary from './route-handlers/summary';
67
import * as Teams from './route-handlers/teams';
78
import * as Users from './route-handlers/users';
@@ -11,6 +12,7 @@ export default function () {
1112
Crates.register(this);
1213
DocsRS.register(this);
1314
Keywords.register(this);
15+
Me.register(this);
1416
Summary.register(this);
1517
Teams.register(this);
1618
Users.register(this);

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/factories/crate-ownership.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Factory } from 'ember-cli-mirage';
2+
3+
export default Factory.extend({
4+
emailNotifications: true,
5+
6+
afterCreate(model) {
7+
if (!model.crate) {
8+
throw new Error('Missing `crate` relationship on `crate-ownership`');
9+
}
10+
if (model.team && model.user) {
11+
throw new Error('`team` and `user` on a `crate-ownership` are mutually exclusive');
12+
}
13+
},
14+
});

mirage/factories/mirage-session.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Factory } from 'ember-cli-mirage';
2+
3+
export default Factory.extend({
4+
afterCreate(session) {
5+
if (!session.user) {
6+
throw new Error('Missing `user` relationship');
7+
}
8+
},
9+
});

mirage/factories/user.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,22 @@ export default Factory.extend({
88
return dasherize(this.name);
99
},
1010

11+
email() {
12+
return `${this.login}@crates.io`;
13+
},
14+
1115
url() {
1216
return `https://github.com/${this.login}`;
1317
},
1418

1519
avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4',
20+
21+
emailVerified: null,
22+
emailVerificationToken: null,
23+
24+
afterCreate(model) {
25+
if (model.emailVerified === null) {
26+
model.update({ emailVerified: !model.emailVerificationToken });
27+
}
28+
},
1629
});

mirage/fixtures/crate-ownerships.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export default [
2+
{
3+
crateId: 'nanomsg',
4+
teamId: 1,
5+
},
6+
{
7+
crateId: 'nanomsg',
8+
teamId: 303,
9+
},
10+
{
11+
crateId: 'nanomsg',
12+
userId: 2,
13+
},
14+
{
15+
crateId: 'nanomsg',
16+
userId: 303,
17+
},
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/models/crate-ownership.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Model, belongsTo } from 'ember-cli-mirage';
2+
3+
export default Model.extend({
4+
crate: belongsTo(),
5+
team: belongsTo(),
6+
user: belongsTo(),
7+
});

mirage/models/crate.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,5 @@ import { Model, hasMany } from 'ember-cli-mirage';
33
export default Model.extend({
44
categories: hasMany(),
55
keywords: hasMany(),
6-
teamOwners: hasMany('team'),
76
versions: hasMany(),
8-
userOwners: hasMany('user'),
97
});

mirage/models/mirage-session.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Model, belongsTo } from 'ember-cli-mirage';
2+
3+
/**
4+
* This is a mirage-only model, that is used to keep track of the current
5+
* session and the associated `user` model, because in route handlers we don't
6+
* have access to the cookie data that the actual API is using for these things.
7+
*
8+
* This mock implementation means that there can only ever exist one
9+
* session at a time.
10+
*/
11+
export default Model.extend({
12+
user: belongsTo(),
13+
});

mirage/route-handlers/crates.js

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ export function register(server) {
1818

1919
if (request.queryParams.user_id) {
2020
let userId = parseInt(request.queryParams.user_id, 10);
21-
crates = crates.filter(crate => (crate._owner_users || []).indexOf(userId) !== -1);
21+
crates = crates.filter(crate => schema.crateOwnerships.findBy({ crateId: crate.id, userId }));
2222
}
2323

2424
if (request.queryParams.team_id) {
2525
let teamId = parseInt(request.queryParams.team_id, 10);
26-
crates = crates.filter(crate => (crate._owner_teams || []).indexOf(teamId) !== -1);
26+
crates = crates.filter(crate => schema.crateOwnerships.findBy({ crateId: crate.id, teamId }));
2727
}
2828

2929
if (request.queryParams.sort === 'alpha') {
@@ -99,27 +99,31 @@ export function register(server) {
9999
let crate = schema.crates.find(crateId);
100100
if (!crate) return notFound();
101101

102-
let response = this.serialize(crate.userOwners);
102+
let ownerships = schema.crateOwnerships.where({ crateId }).filter(it => it.userId).models;
103103

104-
response.users.forEach(user => {
105-
user.kind = 'user';
106-
});
107-
108-
return response;
104+
return {
105+
users: ownerships.map(it => {
106+
let json = this.serialize(it.user, 'user').user;
107+
json.kind = 'user';
108+
return json;
109+
}),
110+
};
109111
});
110112

111113
server.get('/api/v1/crates/:crate_id/owner_team', function (schema, request) {
112114
let crateId = request.params.crate_id;
113115
let crate = schema.crates.find(crateId);
114116
if (!crate) return notFound();
115117

116-
let response = this.serialize(crate.teamOwners);
118+
let ownerships = schema.crateOwnerships.where({ crateId }).filter(it => it.teamId).models;
117119

118-
response.teams.forEach(team => {
119-
team.kind = 'team';
120-
});
121-
122-
return response;
120+
return {
121+
teams: ownerships.map(it => {
122+
let json = this.serialize(it.team, 'team').team;
123+
json.kind = 'team';
124+
return json;
125+
}),
126+
};
123127
});
124128

125129
server.get('/api/v1/crates/:crate_id/reverse_dependencies', function (schema, request) {

mirage/route-handlers/me.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { Response } from 'ember-cli-mirage';
2+
3+
import { getSession } from '../utils/session';
4+
5+
export function register(server) {
6+
server.get('/api/v1/me', function (schema) {
7+
let { user } = getSession(schema);
8+
if (!user) {
9+
return new Response(403, {}, { errors: [{ detail: 'must be logged in to perform that action' }] });
10+
}
11+
12+
let ownerships = schema.crateOwnerships.where({ userId: user.id }).models;
13+
14+
let json = this.serialize(user);
15+
16+
json.owned_crates = ownerships.map(ownership => ({
17+
id: ownership.crate.id,
18+
name: ownership.crate.name,
19+
email_notifications: ownership.emailNotifications,
20+
}));
21+
22+
return json;
23+
});
24+
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+
62+
server.put('/api/v1/confirm/:token', (schema, request) => {
63+
let { token } = request.params;
64+
65+
let user = schema.users.findBy({ emailVerificationToken: token });
66+
if (!user) {
67+
return new Response(400, {}, { errors: [{ detail: 'Email belonging to token not found.' }] });
68+
}
69+
70+
user.update({ emailVerified: true, emailVerificationToken: null });
71+
72+
return { ok: true };
73+
});
74+
}

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+
});

mirage/serializers/team.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
},
21+
});

mirage/serializers/user.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
let removePrivateData = this.request.url !== '/api/v1/me';
8+
9+
if (Array.isArray(hash)) {
10+
for (let resource of hash) {
11+
this._adjust(resource, { removePrivateData });
12+
}
13+
} else {
14+
this._adjust(hash, { removePrivateData });
15+
}
16+
17+
return [hash, addToIncludes];
18+
},
19+
20+
_adjust(hash, { removePrivateData }) {
21+
hash.id = Number(hash.id);
22+
23+
if (removePrivateData) {
24+
delete hash.email;
25+
delete hash.email_verified;
26+
} else {
27+
hash.email_verification_sent = hash.email_verified || Boolean(hash.email_verification_token);
28+
}
29+
30+
delete hash.email_verification_token;
31+
},
32+
});

mirage/utils/session.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export function getSession(schema) {
2+
let session = schema.mirageSessions.first();
3+
if (!session || Date.parse(session.expires) < Date.now()) {
4+
return {};
5+
}
6+
7+
let user = schema.users.find(session.userId);
8+
return { session, user };
9+
}

0 commit comments

Comments
 (0)