Skip to content

Commit

Permalink
feat: restrict event creation (freeCodeCamp#1160)
Browse files Browse the repository at this point in the history
* refactor: remove unused cypress plugins file

* refactor(cypress): move event creation to chapter

The subpath is relative to the chapter dashboard, so it seems like a
better fit.

* refactor: rename helper

* refactor(cypress): get response from createEvent

* fix(test): seed and login before creating events

* test: only let authorized members create events

* feat: restrict event creation to auth'd users

* fix(seed): only generate one admin for one chapter

Allows us to test if they can affect other chapters.

* feat: restrict event creation

* fix(test): match new graphql schema

* refactor: use Permission

* fix(test): pass chapterId to cy.createEvent

* refactor: remove comment

* fix: typo

Co-authored-by: Krzysztof G. <[email protected]>

* refactor: clarify comment

* refactor: DRY out assertions

Co-authored-by: Krzysztof G. <[email protected]>

Co-authored-by: Krzysztof G. <[email protected]>
  • Loading branch information
ojeytonwilliams and gikf authored Jul 8, 2022
1 parent 1dac7e2 commit c80e79d
Show file tree
Hide file tree
Showing 11 changed files with 198 additions and 231 deletions.
32 changes: 16 additions & 16 deletions client/graphql.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -884,22 +884,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "chapter_id",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "description",
"description": null,
Expand Down Expand Up @@ -2950,6 +2934,22 @@
"name": "createEvent",
"description": null,
"args": [
{
"name": "chapterId",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "data",
"description": null,
Expand Down
8 changes: 5 additions & 3 deletions client/src/generated/graphql.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ export type CreateChapterInputs = {

export type CreateEventInputs = {
capacity: Scalars['Float'];
chapter_id: Scalars['Int'];
description: Scalars['String'];
ends_at: Scalars['DateTime'];
image_url: Scalars['String'];
Expand Down Expand Up @@ -330,6 +329,7 @@ export type MutationCreateChapterArgs = {
};

export type MutationCreateEventArgs = {
chapterId: Scalars['Int'];
data: CreateEventInputs;
};

Expand Down Expand Up @@ -867,6 +867,7 @@ export type ChapterRolesQuery = {
};

export type CreateEventMutationVariables = Exact<{
chapterId: Scalars['Int'];
data: CreateEventInputs;
}>;

Expand Down Expand Up @@ -2277,8 +2278,8 @@ export type ChapterRolesQueryResult = Apollo.QueryResult<
ChapterRolesQueryVariables
>;
export const CreateEventDocument = gql`
mutation createEvent($data: CreateEventInputs!) {
createEvent(data: $data) {
mutation createEvent($chapterId: Int!, $data: CreateEventInputs!) {
createEvent(chapterId: $chapterId, data: $data) {
id
name
canceled
Expand Down Expand Up @@ -2313,6 +2314,7 @@ export type CreateEventMutationFn = Apollo.MutationFunction<
* @example
* const [createEventMutation, { data, loading, error }] = useCreateEventMutation({
* variables: {
* chapterId: // value for 'chapterId'
* data: // value for 'data'
* },
* });
Expand Down
4 changes: 2 additions & 2 deletions client/src/modules/dashboard/Events/graphql/mutations.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { gql } from '@apollo/client';

export const createEvent = gql`
mutation createEvent($data: CreateEventInputs!) {
createEvent(data: $data) {
mutation createEvent($chapterId: Int!, $data: CreateEventInputs!) {
createEvent(chapterId: $chapterId, data: $data) {
id
name
canceled
Expand Down
3 changes: 1 addition & 2 deletions client/src/modules/dashboard/Events/pages/NewEventPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,9 @@ export const NewEventPage: NextPage = () => {
streaming_url: isOnline(data.venue_type) ? data.streaming_url : null,
tags: tagsArray,
sponsor_ids: sponsorArray,
chapter_id: chapterId,
};
const event = await createEvent({
variables: { data: { ...eventData } },
variables: { chapterId, data: { ...eventData } },
});

if (event.data) {
Expand Down
146 changes: 146 additions & 0 deletions cypress/e2e/dashboard/chapters/[chapterId]/index.cy.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,152 @@
import { expectToBeRejected } from '../../../../support/util';

const testEvent = {
title: 'Test Event',
description: 'Test Description',
url: 'https://test.event.org',
streamingUrl: 'https://test.event.org/video',
capacity: '10',
tags: 'Test, Event, Tag',
startAt: '2022-01-01T00:01',
endAt: '2022-01-02T00:02',
venueId: '1',
imageUrl: 'https://test.event.org/image',
};

// TODO: Consolidate fixtures.
const eventData = {
venue_id: 1,
sponsor_ids: [],
name: 'Other Event',
description: 'Test Description',
url: 'https://test.event.org',
venue_type: 'PhysicalAndOnline',
capacity: 10,
image_url: 'https://test.event.org/image',
streaming_url: 'https://test.event.org/video',
start_at: '2022-01-01T00:01',
ends_at: '2022-01-02T00:02',
tags: 'Test, Event, Tag',
};

describe('chapter dashboard', () => {
beforeEach(() => {
cy.exec('npm run db:seed');
cy.login();
});

it('should have link to add event for chapter', () => {
cy.visit('/dashboard/chapters/1');
cy.get('a[href="/dashboard/chapters/1/new_event"').should('be.visible');
});

it('emails interested users when an event is created', () => {
createEventViaUI(1);
cy.location('pathname').should('match', /^\/dashboard\/events\/\d+$/);
// confirm that the test data appears in the new event
cy.wrap(Object.entries(testEvent)).each(([key, value]) => {
// TODO: simplify this conditional when tags and dates are handled
// properly.
if (!['tags', 'startAt', 'endAt', 'venueId'].includes(key)) {
cy.contains(value);
}
});
// check that the title we selected is in the event we created.
cy.get('@venueTitle').then((venueTitle) => {
cy.contains(venueTitle);
});

// check that the subscribed users have been emailed
cy.waitUntilMail('allMail');

cy.get('@allMail').mhFirst().as('invitation');
// TODO: select chapter during event creation and use that here (much like @venueTitle
// ) i.e. remove the hardcoding.
cy.getChapterMembers(1).then((members) => {
const subscriberEmails = members
.filter(({ subscribed }) => subscribed)
.map(({ user: { email } }) => email);
cy.get('@invitation')
.mhGetRecipients()
.should('have.members', subscriberEmails);
});
cy.get('@invitation').then((mail) => {
cy.checkBcc(mail).should('eq', true);
});
});

it('prevents members and admins from other chapters from creating events', () => {
let chapterId = 2;
// normal member
cy.register();
cy.login(Cypress.env('JWT_TEST_USER'));
cy.createEvent(chapterId, eventData).then(expectToBeRejected);

// admin of a different chapter
cy.login(Cypress.env('JWT_ADMIN_USER'));
cy.createEvent(2, eventData).then(expectToBeRejected);

// switch the chapterId to match the admin's chapter
chapterId = 1;
cy.createEvent(chapterId, {
...eventData,
name: 'Created by Admin',
}).then((response) => {
expect(response.status).to.eq(200);
expect(response.body.errors).not.to.exist;

cy.visit(`/dashboard/events/`);
cy.contains('Created by Admin');
cy.contains(eventData.name).should('not.exist');
});
});

function createEventViaUI(chapterId) {
cy.visit(`/dashboard/chapters/${chapterId}`);
cy.get(`a[href="/dashboard/chapters/${chapterId}/new_event"]`).click();
cy.findByRole('textbox', { name: 'Event title' }).type(testEvent.title);
cy.findByRole('textbox', { name: 'Description' }).type(
testEvent.description,
);
cy.findByRole('textbox', { name: 'Event Image Url' }).type(
testEvent.imageUrl,
);
cy.findByRole('textbox', { name: 'Url' }).type(testEvent.url);
cy.findByRole('spinbutton', { name: 'Capacity' }).type(testEvent.capacity);
cy.findByRole('textbox', { name: 'Tags (separated by a comma)' }).type(
'Test, Event, Tag',
);

cy.findByLabelText(/^Start at/)
.clear()
.type(testEvent.startAt)
.type('{esc}');
cy.findByLabelText(/^End at/)
.clear()
.type(testEvent.endAt)
.type('{esc}');

// TODO: figure out why cypress thinks this is covered.
// cy.findByRole('checkbox', { name: 'Invite only' }).click();
cy.get('[data-cy="invite-only-checkbox"]').click();
// TODO: I thought <select> would be a listbox - does it matter that it's a
// combobox?
cy.findByRole('combobox', { name: 'Venue' })
.as('venueSelect')
.select(testEvent.venueId);
cy.get('@venueSelect')
.find(`option[value=${testEvent.venueId}]`)
.invoke('text')
.as('venueTitle');
cy.findByRole('textbox', { name: 'Streaming URL' }).type(
testEvent.streamingUrl,
);

cy.findByRole('form', { name: 'Add event' })
.findByRole('button', {
name: 'Add event',
})
.click();
cy.location('pathname').should('match', /^\/dashboard\/events\/\d+$/);
}
});
103 changes: 3 additions & 100 deletions cypress/e2e/dashboard/events/index.cy.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,3 @@
const testEvent = {
title: 'Test Event',
description: 'Test Description',
url: 'https://test.event.org',
streamingUrl: 'https://test.event.org/video',
capacity: '10',
tags: 'Test, Event, Tag',
startAt: '2022-01-01T00:01',
endAt: '2022-01-02T00:02',
venueId: '1',
imageUrl: 'https://test.event.org/image',
};

describe('events dashboard', () => {
beforeEach(() => {
cy.exec('npm run db:seed');
Expand All @@ -25,7 +12,7 @@ describe('events dashboard', () => {
cy.get('a[aria-current="page"]').should('have.text', 'Events');
});

it('should have a table with links to view, create and edit events', () => {
it('should have a table with links to view and edit events', () => {
cy.visit('/dashboard/events');
cy.findByRole('table', { name: 'Events' }).should('be.visible');
cy.findByRole('columnheader', { name: 'name' }).should('be.visible');
Expand All @@ -34,90 +21,6 @@ describe('events dashboard', () => {
cy.get('a[href="/dashboard/events/1/edit"]').should('be.visible');
});

it('emails interested users when an event is created', () => {
createEvent(1);
cy.location('pathname').should('match', /^\/dashboard\/events\/\d+$/);
// confirm that the test data appears in the new event
cy.wrap(Object.entries(testEvent)).each(([key, value]) => {
// TODO: simplify this conditional when tags and dates are handled
// properly.
if (!['tags', 'startAt', 'endAt', 'venueId'].includes(key)) {
cy.contains(value);
}
});
// check that the title we selected is in the event we created.
cy.get('@venueTitle').then((venueTitle) => {
cy.contains(venueTitle);
});

// check that the subscribed users have been emailed
cy.waitUntilMail('allMail');

cy.get('@allMail').mhFirst().as('invitation');
// TODO: select chapter during event creation and use that here (much like @venueTitle
// ) i.e. remove the hardcoding.
cy.getChapterMembers(1).then((members) => {
const subscriberEmails = members
.filter(({ subscribed }) => subscribed)
.map(({ user: { email } }) => email);
cy.get('@invitation')
.mhGetRecipients()
.should('have.members', subscriberEmails);
});
cy.get('@invitation').then((mail) => {
cy.checkBcc(mail).should('eq', true);
});
});

function createEvent(chapterId) {
cy.visit(`/dashboard/chapters/${chapterId}`);
cy.get(`a[href="/dashboard/chapters/${chapterId}/new_event"]`).click();
cy.findByRole('textbox', { name: 'Event title' }).type(testEvent.title);
cy.findByRole('textbox', { name: 'Description' }).type(
testEvent.description,
);
cy.findByRole('textbox', { name: 'Event Image Url' }).type(
testEvent.imageUrl,
);
cy.findByRole('textbox', { name: 'Url' }).type(testEvent.url);
cy.findByRole('spinbutton', { name: 'Capacity' }).type(testEvent.capacity);
cy.findByRole('textbox', { name: 'Tags (separated by a comma)' }).type(
'Test, Event, Tag',
);

cy.findByLabelText(/^Start at/)
.clear()
.type(testEvent.startAt)
.type('{esc}');
cy.findByLabelText(/^End at/)
.clear()
.type(testEvent.endAt)
.type('{esc}');

// TODO: figure out why cypress thinks this is covered.
// cy.findByRole('checkbox', { name: 'Invite only' }).click();
cy.get('[data-cy="invite-only-checkbox"]').click();
// TODO: I thought <select> would be a listbox - does it matter that it's a
// combobox?
cy.findByRole('combobox', { name: 'Venue' })
.as('venueSelect')
.select(testEvent.venueId);
cy.get('@venueSelect')
.find(`option[value=${testEvent.venueId}]`)
.invoke('text')
.as('venueTitle');
cy.findByRole('textbox', { name: 'Streaming URL' }).type(
testEvent.streamingUrl,
);

cy.findByRole('form', { name: 'Add event' })
.findByRole('button', {
name: 'Add event',
})
.click();
cy.location('pathname').should('match', /^\/dashboard\/events\/\d+$/);
}

it('has a button to email attendees', () => {
cy.visit('/dashboard/events/1');
// sending to confirmed first
Expand Down Expand Up @@ -203,7 +106,6 @@ describe('events dashboard', () => {
it("emails the users when an event's venue is changed", () => {
const eventData = {
venue_id: 1,
chapter_id: 1,
sponsor_ids: [],
name: 'Event Venue change test',
description: 'Test Description',
Expand All @@ -218,7 +120,8 @@ describe('events dashboard', () => {
};
const newVenueId = 2;

cy.createEvent(eventData).then((eventId) => {
cy.createEvent(1, eventData).then((response) => {
const eventId = response.body.data.createEvent.id;
cy.visit(`/dashboard/events/${eventId}/edit`);
cy.findByRole('combobox', { name: 'Venue' })
.select(newVenueId)
Expand Down
Loading

0 comments on commit c80e79d

Please sign in to comment.