diff --git a/mirage/factories/crate-owner-invitation.js b/mirage/factories/crate-owner-invitation.js new file mode 100644 index 00000000000..cbc4d43ef69 --- /dev/null +++ b/mirage/factories/crate-owner-invitation.js @@ -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}\``); + } + }, +}); diff --git a/mirage/models/crate-owner-invitation.js b/mirage/models/crate-owner-invitation.js new file mode 100644 index 00000000000..ec84c11d4d0 --- /dev/null +++ b/mirage/models/crate-owner-invitation.js @@ -0,0 +1,7 @@ +import { belongsTo, Model } from 'ember-cli-mirage'; + +export default Model.extend({ + crate: belongsTo(), + invitee: belongsTo('user'), + inviter: belongsTo('user'), +}); diff --git a/mirage/models/crate-owner-invite.js b/mirage/models/crate-owner-invite.js deleted file mode 100644 index 770b50936d3..00000000000 --- a/mirage/models/crate-owner-invite.js +++ /dev/null @@ -1,3 +0,0 @@ -import { Model } from 'ember-cli-mirage'; - -export default Model.extend({}); diff --git a/mirage/route-handlers/me.js b/mirage/route-handlers/me.js index 8739915af52..b5dde2731ee 100644 --- a/mirage/route-handlers/me.js +++ b/mirage/route-handlers/me.js @@ -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 } }; + }); } diff --git a/mirage/serializers/crate-owner-invitation.js b/mirage/serializers/crate-owner-invitation.js new file mode 100644 index 00000000000..61c13dd4d09 --- /dev/null +++ b/mirage/serializers/crate-owner-invitation.js @@ -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; + }, +}); diff --git a/tests/acceptance/invites-test.js b/tests/acceptance/invites-test.js index cf5a0ac1ce5..1d2513d8bf4 100644 --- a/tests/acceptance/invites-test.js +++ b/tests/acceptance/invites-test.js @@ -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) { @@ -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'); @@ -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'); @@ -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'); @@ -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'); @@ -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'); @@ -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'); diff --git a/tests/acceptance/token-invites-test.js b/tests/acceptance/token-invites-test.js index a8eb6f5bc55..448631e89e5 100644 --- a/tests/acceptance/token-invites-test.js +++ b/tests/acceptance/token-invites-test.js @@ -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'; @@ -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.'); @@ -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( diff --git a/tests/mirage/invitations-test.js b/tests/mirage/invitations-test.js new file mode 100644 index 00000000000..491a95991b4 --- /dev/null +++ b/tests/mirage/invitations-test.js @@ -0,0 +1,103 @@ +import { module, test } from 'qunit'; + +import fetch from 'fetch'; + +import { setupTest } from 'cargo/tests/helpers'; + +import setupMirage from '../helpers/setup-mirage'; + +module('Mirage | Crate Owner Invitations', function (hooks) { + setupTest(hooks); + setupMirage(hooks); + + module('GET /api/v1/me/crate_owner_invitations', function () { + test('empty case', async function (assert) { + let user = this.server.create('user'); + this.server.create('mirage-session', { user }); + + let response = await fetch('/api/v1/me/crate_owner_invitations'); + assert.equal(response.status, 200); + + let responsePayload = await response.json(); + assert.deepEqual(responsePayload, { crate_owner_invitations: [] }); + }); + + test('returns the list of invitations for the authenticated user', async function (assert) { + let nanomsg = this.server.create('crate', { name: 'nanomsg' }); + this.server.create('version', { crate: nanomsg }); + + let ember = this.server.create('crate', { name: 'ember-rs' }); + this.server.create('version', { crate: ember }); + + let user = this.server.create('user'); + this.server.create('mirage-session', { user }); + + let inviter = this.server.create('user', { name: 'janed' }); + this.server.create('crate-owner-invitation', { + crate: nanomsg, + createdAt: '2016-12-24T12:34:56Z', + invitee: user, + inviter, + }); + + let inviter2 = this.server.create('user', { name: 'wycats' }); + this.server.create('crate-owner-invitation', { + crate: ember, + createdAt: '2020-12-31T12:34:56Z', + invitee: user, + inviter: inviter2, + }); + + let response = await fetch('/api/v1/me/crate_owner_invitations'); + assert.equal(response.status, 200); + + let responsePayload = await response.json(); + assert.deepEqual(responsePayload, { + crate_owner_invitations: [ + { + crate_id: nanomsg.id, + crate_name: 'nanomsg', + created_at: '2016-12-24T12:34:56Z', + invited_by_username: 'janed', + invitee_id: Number(user.id), + inviter_id: Number(inviter.id), + }, + { + crate_id: ember.id, + crate_name: 'ember-rs', + created_at: '2020-12-31T12:34:56Z', + invited_by_username: 'wycats', + invitee_id: Number(user.id), + inviter_id: Number(inviter2.id), + }, + ], + users: [ + { + avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', + id: Number(inviter.id), + login: 'janed', + name: 'janed', + url: 'https://github.com/janed', + }, + { + avatar: 'https://avatars1.githubusercontent.com/u/14631425?v=4', + id: Number(inviter2.id), + login: 'wycats', + name: 'wycats', + url: 'https://github.com/wycats', + }, + ], + }); + }); + + test('returns an error if unauthenticated', async function (assert) { + let response = await fetch('/api/v1/me/crate_owner_invitations'); + assert.equal(response.status, 403); + + let responsePayload = await response.json(); + assert.deepEqual(responsePayload, { + errors: [{ detail: 'must be logged in to perform that action' }], + }); + }); + }); +});