Skip to content

mirage: Implement crate_owner_invitations endpoints #3707

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jun 16, 2021
Merged
18 changes: 18 additions & 0 deletions mirage/factories/crate-owner-invitation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Factory } from 'ember-cli-mirage';

export default Factory.extend({
createdAt: '2016-12-24T12:34:56Z',
token: i => `secret-token-${i}`,

afterCreate(invite) {
if (!invite.crateId) {
throw new Error(`Missing \`crate\` relationship on \`crate-owner-invitation:${invite.id}\``);
}
if (!invite.inviteeId) {
throw new Error(`Missing \`invitee\` relationship on \`crate-owner-invitation:${invite.id}\``);
}
if (!invite.inviterId) {
throw new Error(`Missing \`inviter\` relationship on \`crate-owner-invitation:${invite.id}\``);
}
},
});
7 changes: 7 additions & 0 deletions mirage/models/crate-owner-invitation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { belongsTo, Model } from 'ember-cli-mirage';

export default Model.extend({
crate: belongsTo(),
invitee: belongsTo('user'),
inviter: belongsTo('user'),
});
3 changes: 0 additions & 3 deletions mirage/models/crate-owner-invite.js

This file was deleted.

46 changes: 46 additions & 0 deletions mirage/route-handlers/me.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,50 @@ export function register(server) {

return { ok: true };
});

server.get('/api/v1/me/crate_owner_invitations', function (schema) {
let { user } = getSession(schema);
if (!user) {
return new Response(403, {}, { errors: [{ detail: 'must be logged in to perform that action' }] });
}

return schema.crateOwnerInvitations.where({ inviteeId: user.id });
});

server.put('/api/v1/me/crate_owner_invitations/:crate_id', (schema, request) => {
let { user } = getSession(schema);
if (!user) {
return new Response(403, {}, { errors: [{ detail: 'must be logged in to perform that action' }] });
}

let body = JSON.parse(request.requestBody);
let { accepted, crate_id: crateId } = body.crate_owner_invite;

let invite = schema.crateOwnerInvitations.findBy({ crateId, inviteeId: user.id });
if (!invite) {
return new Response(404);
}

if (accepted) {
server.create('crate-ownership', { crate: invite.crate, user });
}

invite.destroy();

return { crate_owner_invitation: { crate_id: crateId, accepted } };
});

server.put('/api/v1/me/crate_owner_invitations/accept/:token', (schema, request) => {
let { token } = request.params;

let invite = schema.crateOwnerInvitations.findBy({ token });
if (!invite) {
return new Response(404);
}

server.create('crate-ownership', { crate: invite.crate, user: invite.invitee });
invite.destroy();

return { crate_owner_invitation: { crate_id: invite.crateId, accepted: true } };
});
}
34 changes: 34 additions & 0 deletions mirage/serializers/crate-owner-invitation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import BaseSerializer from './application';

export default BaseSerializer.extend({
// eslint-disable-next-line ember/avoid-leaking-state-in-ember-objects
include: ['inviter'],

getHashForResource() {
let [hash, addToIncludes] = BaseSerializer.prototype.getHashForResource.apply(this, arguments);

if (Array.isArray(hash)) {
for (let resource of hash) {
this._adjust(resource);
}
} else {
this._adjust(hash);
}

return [hash, addToIncludes];
},

_adjust(hash) {
delete hash.id;
delete hash.token;

let crate = this.schema.crates.find(hash.crate_id);
hash.crate_name = crate.name;

hash.invitee_id = Number(hash.invitee_id);
hash.inviter_id = Number(hash.inviter_id);

let inviter = this.schema.users.find(hash.inviter_id);
hash.invited_by_username = inviter.login;
},
});
96 changes: 41 additions & 55 deletions tests/acceptance/invites-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,36 +12,32 @@ module('Acceptance | /me/pending-invites', function (hooks) {
setupApplicationTest(hooks);

function prepare(context) {
let user = context.server.create('user');
context.authenticateAs(user);

let inviter = context.server.create('user', { name: 'janed' });
let inviter2 = context.server.create('user', { name: 'wycats' });
context.server.get('/api/v1/me/crate_owner_invitations', function () {
let users = [this.serialize(inviter, 'user').user, this.serialize(inviter2, 'user').user];

return {
crate_owner_invitations: [
{
invited_by_username: 'janed',
crate_name: 'nanomsg',
crate_id: 42,
created_at: '2016-12-24T12:34:56Z',
invitee_id: parseInt(user.id, 10),
inviter_id: parseInt(inviter.id, 10),
},
{
invited_by_username: 'wycats',
crate_name: 'ember-rs',
crate_id: 1,
created_at: '2020-12-31T12:34:56Z',
invitee_id: parseInt(user.id, 10),
inviter_id: parseInt(inviter2.id, 10),
},
],
users,
};

let user = context.server.create('user');

let nanomsg = context.server.create('crate', { name: 'nanomsg' });
context.server.create('version', { crate: nanomsg });
context.server.create('crate-owner-invitation', {
crate: nanomsg,
createdAt: '2016-12-24T12:34:56Z',
invitee: user,
inviter,
});

let ember = context.server.create('crate', { name: 'ember-rs' });
context.server.create('version', { crate: ember });
context.server.create('crate-owner-invitation', {
crate: ember,
createdAt: '2020-12-31T12:34:56Z',
invitee: user,
inviter: inviter2,
});

context.authenticateAs(user);

return { nanomsg, user };
}

test('redirects to / when not logged in', async function (assert) {
Expand Down Expand Up @@ -76,7 +72,7 @@ module('Acceptance | /me/pending-invites', function (hooks) {
test('shows empty list message', async function (assert) {
prepare(this);

this.server.get('/api/v1/me/crate_owner_invitations', { crate_owner_invitations: [] });
this.server.schema.crateOwnerInvitations.all().destroy();

await visit('/me/pending-invites');
assert.equal(currentURL(), '/me/pending-invites');
Expand All @@ -85,19 +81,11 @@ module('Acceptance | /me/pending-invites', function (hooks) {
});

test('invites can be declined', async function (assert) {
assert.expect(9);
let { nanomsg, user } = prepare(this);

prepare(this);

this.server.put('/api/v1/me/crate_owner_invitations/:crate', (schema, request) => {
assert.deepEqual(request.params, { crate: '42' });

let body = JSON.parse(request.requestBody);
assert.strictEqual(body.crate_owner_invite.accepted, false);
assert.strictEqual(body.crate_owner_invite.crate_id, 42);

return { crate_owner_invitation: { crate_id: 42, accepted: false } };
});
let { crateOwnerInvitations, crateOwnerships } = this.server.schema;
assert.equal(crateOwnerInvitations.where({ crateId: nanomsg.id, inviteeId: user.id }).length, 1);
assert.equal(crateOwnerships.where({ crateId: nanomsg.id, userId: user.id }).length, 0);

await visit('/me/pending-invites');
assert.equal(currentURL(), '/me/pending-invites');
Expand All @@ -110,12 +98,15 @@ module('Acceptance | /me/pending-invites', function (hooks) {
.hasText('Declined. You have not been added as an owner of crate nanomsg.');
assert.dom('[data-test-invite="nanomsg"] [data-test-crate-link]').doesNotExist();
assert.dom('[data-test-invite="nanomsg"] [data-test-inviter-link]').doesNotExist();

assert.equal(crateOwnerInvitations.where({ crateId: nanomsg.id, inviteeId: user.id }).length, 0);
assert.equal(crateOwnerships.where({ crateId: nanomsg.id, userId: user.id }).length, 0);
});

test('error message is shown if decline request fails', async function (assert) {
prepare(this);

this.server.put('/api/v1/me/crate_owner_invitations/:crate', () => new Response(500));
this.server.put('/api/v1/me/crate_owner_invitations/:crate_id', () => new Response(500));

await visit('/me/pending-invites');
assert.equal(currentURL(), '/me/pending-invites');
Expand All @@ -127,19 +118,11 @@ module('Acceptance | /me/pending-invites', function (hooks) {
});

test('invites can be accepted', async function (assert) {
assert.expect(9);
let { nanomsg, user } = prepare(this);

prepare(this);

this.server.put('/api/v1/me/crate_owner_invitations/:crate', (schema, request) => {
assert.deepEqual(request.params, { crate: '42' });

let body = JSON.parse(request.requestBody);
assert.strictEqual(body.crate_owner_invite.accepted, true);
assert.strictEqual(body.crate_owner_invite.crate_id, 42);

return { crate_owner_invitation: { crate_id: 42, accepted: true } };
});
let { crateOwnerInvitations, crateOwnerships } = this.server.schema;
assert.equal(crateOwnerInvitations.where({ crateId: nanomsg.id, inviteeId: user.id }).length, 1);
assert.equal(crateOwnerships.where({ crateId: nanomsg.id, userId: user.id }).length, 0);

await visit('/me/pending-invites');
assert.equal(currentURL(), '/me/pending-invites');
Expand All @@ -154,12 +137,15 @@ module('Acceptance | /me/pending-invites', function (hooks) {
assert.dom('[data-test-invite="nanomsg"] [data-test-inviter-link]').doesNotExist();

await percySnapshot(assert);

assert.equal(crateOwnerInvitations.where({ crateId: nanomsg.id, inviteeId: user.id }).length, 0);
assert.equal(crateOwnerships.where({ crateId: nanomsg.id, userId: user.id }).length, 1);
});

test('error message is shown if accept request fails', async function (assert) {
prepare(this);

this.server.put('/api/v1/me/crate_owner_invitations/:crate', () => new Response(500));
this.server.put('/api/v1/me/crate_owner_invitations/:crate_id', () => new Response(500));

await visit('/me/pending-invites');
assert.equal(currentURL(), '/me/pending-invites');
Expand All @@ -176,7 +162,7 @@ module('Acceptance | /me/pending-invites', function (hooks) {
let errorMessage =
'The invitation to become an owner of the demo_crate crate expired. Please reach out to an owner of the crate to request a new invitation.';
let payload = { errors: [{ detail: errorMessage }] };
this.server.put('/api/v1/me/crate_owner_invitations/:crate', payload, 410);
this.server.put('/api/v1/me/crate_owner_invitations/:crate_id', payload, 410);

await visit('/me/pending-invites');
assert.equal(currentURL(), '/me/pending-invites');
Expand Down
22 changes: 7 additions & 15 deletions tests/acceptance/token-invites-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { currentURL } from '@ember/test-helpers';
import { module, test } from 'qunit';

import percySnapshot from '@percy/ember';
import Response from 'ember-cli-mirage/response';

import { setupApplicationTest } from 'cargo/tests/helpers';

Expand All @@ -24,13 +23,6 @@ module('Acceptance | /accept-invite/:token', function (hooks) {
});

test('shows error for unknown token', async function (assert) {
assert.expect(3);

this.server.put('/api/v1/me/crate_owner_invitations/accept/:token', (schema, request) => {
assert.deepEqual(request.params, { token: 'unknown' });
return new Response(404);
});

await visit('/accept-invite/unknown');
assert.equal(currentURL(), '/accept-invite/unknown');
assert.dom('[data-test-error-message]').hasText('You may want to visit crates.io/me/pending-invites to try again.');
Expand All @@ -48,15 +40,15 @@ module('Acceptance | /accept-invite/:token', function (hooks) {
});

test('shows success for known token', async function (assert) {
assert.expect(3);
let inviter = this.server.create('user');
let invitee = this.server.create('user');

this.server.put('/api/v1/me/crate_owner_invitations/accept/:token', (schema, request) => {
assert.deepEqual(request.params, { token: 'ember-rs' });
return { crate_owner_invitation: { crate_id: 42, accepted: true } };
});
let crate = this.server.create('crate', { name: 'nanomsg' });
this.server.create('version', { crate });
let invite = this.server.create('crate-owner-invitation', { crate, invitee, inviter });

await visit('/accept-invite/ember-rs');
assert.equal(currentURL(), '/accept-invite/ember-rs');
await visit(`/accept-invite/${invite.token}`);
assert.equal(currentURL(), `/accept-invite/${invite.token}`);
assert
.dom('[data-test-success-message]')
.hasText(
Expand Down
Loading