From d35ccff5a10bcf01e9e78b883f303998cea3ee43 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Mon, 7 Dec 2020 14:48:48 +0100 Subject: [PATCH] Add tests for participation without account Signed-off-by: Thomas Citharel --- .../ParticipationWithoutAccount.vue | 77 ++++- js/src/graphql/event.ts | 16 + js/src/i18n/en_US.json | 14 +- js/src/i18n/fr_FR.json | 9 +- .../components/Comment/CommentTree.spec.ts | 4 +- .../ParticipationWithoutAccount.spec.ts | 278 ++++++++++++++++++ .../ParticipationWithoutAccount.spec.ts.snap | 126 ++++++++ js/tests/unit/specs/mocks/event.ts | 58 +++- lib/service/workers/notification.ex | 2 + 9 files changed, 562 insertions(+), 22 deletions(-) create mode 100644 js/tests/unit/specs/components/Participation/ParticipationWithoutAccount.spec.ts create mode 100644 js/tests/unit/specs/components/Participation/__snapshots__/ParticipationWithoutAccount.spec.ts.snap diff --git a/js/src/components/Participation/ParticipationWithoutAccount.vue b/js/src/components/Participation/ParticipationWithoutAccount.vue index 83094571d..802b11e7a 100644 --- a/js/src/components/Participation/ParticipationWithoutAccount.vue +++ b/js/src/components/Participation/ParticipationWithoutAccount.vue @@ -81,33 +81,72 @@ {{ $t("Request for participation confirmation sent") }}

- {{ $t("Check your inbox (and your junk mail folder).") }} + {{ + $t("Check your inbox (and your junk mail folder).") + }} + + {{ + $t( + "Your participation will be validated once you click the confirmation link into the email, and after the organizer manually validates your participation." + ) + }} {{ + $t( + "Your participation will be validated once you click the confirmation link into the email." + ) + }} +

+ {{ error }} +

+ + {{ $t("return to the event's page") }} +

-

{{ $t("You may now close this window.") }}

+ {{ + $t( + "Unable to load event for participation. The error details are provided below:" + ) + }} +
+
{{ error }}
+
+
+ diff --git a/js/src/graphql/event.ts b/js/src/graphql/event.ts index 98c508778..1c547802c 100644 --- a/js/src/graphql/event.ts +++ b/js/src/graphql/event.ts @@ -176,6 +176,22 @@ export const FETCH_EVENT = gql` } `; +export const FETCH_EVENT_BASIC = gql` + query($uuid: UUID!) { + event(uuid: $uuid) { + id + uuid + joinOptions + participantStats { + going + notApproved + notConfirmed + participant + } + } + } +`; + export const FETCH_EVENTS = gql` query { events { diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json index a676c79b0..d98ee3686 100644 --- a/js/src/i18n/en_US.json +++ b/js/src/i18n/en_US.json @@ -24,7 +24,6 @@ "Anonymous participant": "Anonymous participant", "Anonymous participants will be asked to confirm their participation through e-mail.": "Anonymous participants will be asked to confirm their participation through e-mail.", "Anonymous participations": "Anonymous participations", - "Approve": "Approve", "Are you really sure you want to delete your whole account? You'll lose everything. Identities, settings, events created, messages and participations will be gone forever.": "Are you really sure you want to delete your whole account? You'll lose everything. Identities, settings, events created, messages and participations will be gone forever.", "Are you sure you want to delete this comment? This action cannot be undone.": "Are you sure you want to delete this comment? This action cannot be undone.", "Are you sure you want to delete this event? This action cannot be undone. You may want to engage the discussion with the event creator or edit its event instead.": "Are you sure you want to delete this event? This action cannot be undone. You may want to engage the discussion with the event creator or edit its event instead.", @@ -70,7 +69,6 @@ "Create my profile": "Create my profile", "Create token": "Create token", "Create": "Create", - "Creator": "Creator", "Current identity has been changed to {identityName} in order to manage this event.": "Current identity has been changed to {identityName} in order to manage this event.", "Current page": "Current page", "Custom URL": "Custom URL", @@ -126,7 +124,6 @@ "Event": "Event", "Events": "Events", "Ex: mobilizon.fr": "Ex: mobilizon.fr", - "Exclude": "Exclude", "Explore": "Explore", "Failed to save admin settings": "Failed to save admin settings", "Featured events": "Featured events", @@ -145,7 +142,6 @@ "General": "General", "Getting location": "Getting location", "Go": "Go", - "Going as {name}": "Going as {name}", "Group name": "Group name", "Group {displayName} created": "Group {displayName} created", "Groups": "Groups", @@ -280,7 +276,6 @@ "Registration is allowed, anyone can register.": "Registration is allowed, anyone can register.", "Registration is closed.": "Registration is closed.", "Registration is currently closed.": "Registration is currently closed.", - "Reject": "Reject", "Rejected": "Rejected", "Reopen": "Reopen", "Reply": "Reply", @@ -749,7 +744,7 @@ "Go to the event page": "Go to the event page", "Request for participation confirmation sent": "Request for participation confirmation sent", "Check your inbox (and your junk mail folder).": "Check your inbox (and your junk mail folder).", - "You may now close this window.": "You may now close this window.", + "You may now close this window, or {return_to_event}.": "You may now close this window, or {return_to_event}.", "Create an account": "Create an account", "You are not an administrator for this group.": "You are not an administrator for this group.", "Why create an account?": "Why create an account?", @@ -810,5 +805,10 @@ "Your participation status is saved only on this device and will be deleted one month after the event's passed.": "Your participation status is saved only on this device and will be deleted one month after the event's passed.", "You may clear all participation information for this device with the buttons below.": "You may clear all participation information for this device with the buttons below.", "Clear participation data for this event": "Clear participation data for this event", - "Clear participation data for all events": "Clear participation data for all events" + "Clear participation data for all events": "Clear participation data for all events", + "Your participation will be validated once you click the confirmation link into the email, and after the organizer manually validates your participation.": "Your participation will be validated once you click the confirmation link into the email, and after the organizer manually validates your participation.", + "Your participation will be validated once you click the confirmation link into the email.": "Your participation will be validated once you click the confirmation link into the email.", + "Unable to load event for participation. The error details are provided below:": "Unable to load event for participation. The error details are provided below:", + "Unable to save your participation in this browser.": "Unable to save your participation in this browser.", + "return to the event's page": "return to the event's page" } diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json index 257a116e2..1da661f47 100644 --- a/js/src/i18n/fr_FR.json +++ b/js/src/i18n/fr_FR.json @@ -794,7 +794,6 @@ "You have one event today.": "Vous n'avez pas d'évenement aujourd'hui | Vous avez un évènement aujourd'hui. | Vous avez {count} évènements aujourd'hui", "You have one event tomorrow.": "Vous n'avez pas d'évènement demain | Vous avez un évènement demain. | Vous avez {count} évènements demain", "You may also ask to {resend_confirmation_email}.": "Vous pouvez aussi demander à {resend_confirmation_email}.", - "You may now close this window.": "Vous pouvez maintenant fermer cette fenêtre.", "You need to create the group before you create an event.": "Vous devez créer le groupe avant de créer l'événement.", "You need to login.": "Vous devez vous connecter.", "You will be able to add an avatar and set other options in your account settings.": "Vous pourrez ajouter un avatar et définir d'autres options dans les paramètres de votre compte.", @@ -898,5 +897,11 @@ "Your participation status is saved only on this device and will be deleted one month after the event's passed.": "Le statut de votre participation est enregistré uniquement sur cet appareil et sera supprimé un mois après la fin de l'événement.", "You may clear all participation information for this device with the buttons below.": "Vous pouvez effacer toutes les informations de participation pour cet appareil avec les boutons ci-dessous.", "Clear participation data for this event": "Effacer mes données de participation pour cet événement", - "Clear participation data for all events": "Effacer mes données de participation pour tous les événements" + "Clear participation data for all events": "Effacer mes données de participation pour tous les événements", + "Your participation will be validated once you click the confirmation link into the email, and after the organizer manually validates your participation.": "Votre participation sera validée une fois que vous aurez cliqué sur le lien de confirmation contenu dans le courriel, et après que l'organisateur·ice valide votre participation.", + "Your participation will be validated once you click the confirmation link into the email.": "Votre participation sera validée une fois que vous aurez cliqué sur le lien de confirmation contenu dans le courriel.", + "Unable to load event for participation. The error details are provided below:": "Impossible de charger l'événement pour la participation. Les détails de l'erreur sont disponibles ci-dessous :", + "Unable to save your participation in this browser.": "Échec de la sauvegarde de votre participation dans ce navigateur.", + "return to the event's page": "retourner sur la page de l'événement", + "You may now close this window, or {return_to_event}.": "Vous pouvez maintenant fermer cette fenêtre, ou bien {return_to_event}." } diff --git a/js/tests/unit/specs/components/Comment/CommentTree.spec.ts b/js/tests/unit/specs/components/Comment/CommentTree.spec.ts index 886c6a443..8706f02ef 100644 --- a/js/tests/unit/specs/components/Comment/CommentTree.spec.ts +++ b/js/tests/unit/specs/components/Comment/CommentTree.spec.ts @@ -102,7 +102,9 @@ describe("CommentTree", () => { await wrapper.vm.$nextTick(); // because of the expect(wrapper.exists()).toBe(true); - expect(requestHandlers.eventCommentThreadsQueryHandler).toHaveBeenCalled(); + expect( + requestHandlers.eventCommentThreadsQueryHandler + ).toHaveBeenCalledWith({ eventUUID: eventData.uuid }); expect(wrapper.vm.$apollo.queries.comments).toBeTruthy(); expect(wrapper.find(".loading").exists()).toBe(false); expect(wrapper.findAll(".comment-list .root-comment").length).toBe(2); diff --git a/js/tests/unit/specs/components/Participation/ParticipationWithoutAccount.spec.ts b/js/tests/unit/specs/components/Participation/ParticipationWithoutAccount.spec.ts new file mode 100644 index 000000000..f69fa7009 --- /dev/null +++ b/js/tests/unit/specs/components/Participation/ParticipationWithoutAccount.spec.ts @@ -0,0 +1,278 @@ +import { config, createLocalVue, mount, Wrapper } from "@vue/test-utils"; +import ParticipationWithoutAccount from "@/components/Participation/ParticipationWithoutAccount.vue"; +import Buefy from "buefy"; +import VueRouter from "vue-router"; +import { routes } from "@/router"; +import { + CommentModeration, + EventJoinOptions, + ParticipantRole, +} from "@/types/enums"; +import { + createMockClient, + MockApolloClient, + RequestHandler, +} from "mock-apollo-client"; +import buildCurrentUserResolver from "@/apollo/user"; +import { InMemoryCache } from "apollo-cache-inmemory"; +import { CONFIG } from "@/graphql/config"; +import VueApollo from "vue-apollo"; +import { FETCH_EVENT_BASIC, JOIN_EVENT } from "@/graphql/event"; +import { IEvent } from "@/types/event.model"; +import { i18n } from "@/utils/i18n"; +import { configMock } from "../../mocks/config"; +import { + fetchEventBasicMock, + joinEventMock, + joinEventResponseMock, +} from "../../mocks/event"; + +const localVue = createLocalVue(); +localVue.use(Buefy); +localVue.use(VueRouter); +const router = new VueRouter({ routes, mode: "history" }); +config.mocks.$t = (key: string): string => key; + +const eventData = { + id: "1", + uuid: "f37910ea-fd5a-4756-9679-00971f3f4106", + options: { + commentModeration: CommentModeration.ALLOW_ALL, + }, + joinOptions: EventJoinOptions.FREE, + beginsOn: new Date("2089-12-04T09:21:25Z"), + endsOn: new Date("2089-12-04T11:21:25Z"), + participantStats: { + notApproved: 0, + notConfirmed: 0, + rejected: 0, + participant: 0, + creator: 1, + moderator: 0, + administrator: 0, + going: 1, + }, +}; + +describe("ParticipationWithoutAccount", () => { + let wrapper: Wrapper; + let mockClient: MockApolloClient; + let apolloProvider; + let requestHandlers: Record; + + const generateWrapper = ( + handlers: Record = {}, + customProps: Record = {}, + baseData: Record = {} + ) => { + const cache = new InMemoryCache({ addTypename: false }); + + mockClient = createMockClient({ + cache, + resolvers: buildCurrentUserResolver(cache), + }); + requestHandlers = { + configQueryHandler: jest.fn().mockResolvedValue(configMock), + fetchEventQueryHandler: jest.fn().mockResolvedValue(fetchEventBasicMock), + joinEventMutationHandler: jest + .fn() + .mockResolvedValue(joinEventResponseMock), + ...handlers, + }; + mockClient.setRequestHandler(CONFIG, requestHandlers.configQueryHandler); + mockClient.setRequestHandler( + FETCH_EVENT_BASIC, + requestHandlers.fetchEventQueryHandler + ); + mockClient.setRequestHandler( + JOIN_EVENT, + requestHandlers.joinEventMutationHandler + ); + + apolloProvider = new VueApollo({ + defaultClient: mockClient, + }); + + wrapper = mount(ParticipationWithoutAccount, { + localVue, + router, + i18n, + apolloProvider, + propsData: { + uuid: eventData.uuid, + ...customProps, + }, + data() { + return { + ...baseData, + }; + }, + }); + }; + + it("renders the participation without account view with minimal data", async () => { + generateWrapper(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + expect(wrapper.exists()).toBe(true); + expect(requestHandlers.configQueryHandler).toHaveBeenCalled(); + expect(wrapper.vm.$apollo.queries.config).toBeTruthy(); + + expect(requestHandlers.fetchEventQueryHandler).toHaveBeenCalledWith({ + uuid: eventData.uuid, + }); + expect(wrapper.vm.$apollo.queries.event).toBeTruthy(); + + expect(wrapper.find(".hero-body .container").isVisible()).toBeTruthy(); + expect(wrapper.find("article.message.is-info").text()).toBe( + "Your email will only be used to confirm that you're a real person and send you eventual updates for this event. It will NOT be transmitted to other instances or to the event organizer." + ); + + wrapper.find('input[type="email"]').setValue("some@email.tld"); + wrapper.find("textarea").setValue("a message long enough"); + wrapper.find("form").trigger("submit"); + + await wrapper.vm.$nextTick(); + + expect(requestHandlers.joinEventMutationHandler).toHaveBeenCalledWith({ + ...joinEventMock, + }); + + const cachedData = mockClient.cache.readQuery<{ event: IEvent }>({ + query: FETCH_EVENT_BASIC, + variables: { + uuid: eventData.uuid, + }, + }); + if (cachedData) { + const { event } = cachedData; + + expect(event.participantStats.going).toBe( + eventData.participantStats.going + 1 + ); + expect(event.participantStats.participant).toBe( + eventData.participantStats.participant + 1 + ); + } + // lots of things to await + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + expect(wrapper.find("form").exists()).toBeFalsy(); + expect(wrapper.find("h1.title").text()).toBe( + "Request for participation confirmation sent" + ); + // TextEncoder is not in js-dom + expect( + wrapper.find("article.message.is-warning .media-content").text() + ).toBe("Unable to save your participation in this browser."); + + expect(wrapper.find("span.details").text()).toBe( + "Your participation will be validated once you click the confirmation link into the email." + ); + expect(wrapper.html()).toMatchSnapshot(); + }); + + it("renders the warning if the event participation is restricted", async () => { + generateWrapper({ + fetchEventQueryHandler: jest.fn().mockResolvedValue({ + data: { + event: { + ...fetchEventBasicMock.data.event, + joinOptions: EventJoinOptions.RESTRICTED, + }, + }, + }), + joinEventMutationHandler: jest.fn().mockResolvedValue({ + data: { + joinEvent: { + ...joinEventResponseMock.data.joinEvent, + role: ParticipantRole.NOT_CONFIRMED, + }, + }, + }), + }); + + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.$data.event.joinOptions).toBe( + EventJoinOptions.RESTRICTED + ); + + expect(wrapper.find(".hero-body .container").text()).toContain( + "The event organizer manually approves participations. Since you've chosen to participate without an account, please explain why you want to participate to this event." + ); + expect(wrapper.find(".hero-body .container").text()).not.toContain( + "If you want, you may send a message to the event organizer here." + ); + + wrapper.find('input[type="email"]').setValue("some@email.tld"); + wrapper.find("textarea").setValue("a message long enough"); + wrapper.find("form").trigger("submit"); + + await wrapper.vm.$nextTick(); + + expect(requestHandlers.joinEventMutationHandler).toHaveBeenCalledWith({ + ...joinEventMock, + }); + + const cachedData = mockClient.cache.readQuery<{ event: IEvent }>({ + query: FETCH_EVENT_BASIC, + variables: { + uuid: eventData.uuid, + }, + }); + if (cachedData) { + const { event } = cachedData; + + expect(event.participantStats.notConfirmed).toBe( + eventData.participantStats.notConfirmed + 1 + ); + } + // lots of things to await + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + expect(wrapper.find("form").exists()).toBeFalsy(); + expect(wrapper.find("h1.title").text()).toBe( + "Request for participation confirmation sent" + ); + expect(wrapper.find("span.details").text()).toBe( + "Your participation will be validated once you click the confirmation link into the email, and after the organizer manually validates your participation." + ); + expect(wrapper.html()).toMatchSnapshot(); + }); + + it("handles being already a participant", async () => { + generateWrapper({ + joinEventMutationHandler: jest + .fn() + .mockRejectedValue( + new Error("You are already a participant of this event") + ), + }); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + wrapper.find('input[type="email"]').setValue("some@email.tld"); + wrapper.find("textarea").setValue("a message long enough"); + wrapper.find("form").trigger("submit"); + + await wrapper.vm.$nextTick(); + + expect(requestHandlers.joinEventMutationHandler).toHaveBeenCalledWith({ + ...joinEventMock, + }); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + expect(wrapper.find("form").exists()).toBeTruthy(); + expect( + wrapper.find("article.message.is-danger .media-content").text() + ).toContain("You are already a participant of this event"); + expect(wrapper.html()).toMatchSnapshot(); + }); +}); diff --git a/js/tests/unit/specs/components/Participation/__snapshots__/ParticipationWithoutAccount.spec.ts.snap b/js/tests/unit/specs/components/Participation/__snapshots__/ParticipationWithoutAccount.spec.ts.snap new file mode 100644 index 000000000..c973aa0ac --- /dev/null +++ b/js/tests/unit/specs/components/Participation/__snapshots__/ParticipationWithoutAccount.spec.ts.snap @@ -0,0 +1,126 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ParticipationWithoutAccount handles being already a participant 1`] = ` +
+
+
+
+

+ This Mobilizon instance and this event organizer allows anonymous participations, but requires validation through email confirmation. +

+ +
+ +
+
+ +
+ Your email will only be used to confirm that you're a real person and send you eventual updates for this event. It will NOT be transmitted to other instances or to the event organizer. +
+
+
+
+
+ +
+ +
+
+ +
You are already a participant of this event
+
+
+
+
+
+
+ + + +
+ +
+

+ If you want, you may send a message to the event organizer here. +

+
+
+ + + +
+ +
+
+ + +
+ +
+
+
+
+`; + +exports[`ParticipationWithoutAccount renders the participation without account view with minimal data 1`] = ` +
+
+
+
+

+ Request for participation confirmation sent +

+

Check your inbox (and your junk mail folder). Your participation will be validated once you click the confirmation link into the email.

+ +
+ +
+
+ +
Unable to save your participation in this browser.
+
+
+
+
+

You may now close this window, or return to the event's page.

+
+
+
+
+`; + +exports[`ParticipationWithoutAccount renders the warning if the event participation is restricted 1`] = ` +
+
+
+
+

+ Request for participation confirmation sent +

+

Check your inbox (and your junk mail folder). + Your participation will be validated once you click the confirmation link into the email, and after the organizer manually validates your participation.

+ +
+ +
+
+ +
Unable to save your participation in this browser.
+
+
+
+
+

You may now close this window, or return to the event's page.

+
+
+
+
+`; diff --git a/js/tests/unit/specs/mocks/event.ts b/js/tests/unit/specs/mocks/event.ts index b9aeebd6b..5c284b037 100644 --- a/js/tests/unit/specs/mocks/event.ts +++ b/js/tests/unit/specs/mocks/event.ts @@ -1,3 +1,59 @@ +import { EventJoinOptions, ParticipantRole } from "@/types/enums"; + +type DataMock = { + data: Record; +}; + +export const fetchEventBasicMock = { + data: { + event: { + __typename: "Event", + id: "1", + uuid: "f37910ea-fd5a-4756-9679-00971f3f4106", + joinOptions: EventJoinOptions.FREE, + participantStats: { + notApproved: 0, + notConfirmed: 0, + rejected: 0, + participant: 0, + creator: 1, + moderator: 0, + administrator: 0, + going: 1, + }, + }, + }, +}; + +export const joinEventResponseMock = { + data: { + joinEvent: { + id: "5", + role: ParticipantRole.NOT_APPROVED, + insertedAt: "2020-12-07T09:33:41Z", + metadata: { + cancellationToken: "some token", + message: "a message long enough", + }, + event: { + id: "1", + uuid: "f37910ea-fd5a-4756-9679-00971f3f4106", + }, + actor: { + id: "1", + }, + }, + }, +}; + +export const joinEventMock = { + eventId: "1", + actorId: "1", + email: "some@email.tld", + message: "a message long enough", + locale: "en_US", +}; + export const eventCommentThreadsMock = { data: { event: { @@ -64,7 +120,7 @@ export const newCommentForEventMock = { inReplyToCommentId: null, }; -export const newCommentForEventResponse = { +export const newCommentForEventResponse: DataMock = { data: { createComment: { id: "79", diff --git a/lib/service/workers/notification.ex b/lib/service/workers/notification.ex index 857ede1bc..a3a7ff9a9 100644 --- a/lib/service/workers/notification.ex +++ b/lib/service/workers/notification.ex @@ -100,6 +100,8 @@ defmodule Mobilizon.Service.Workers.Notification do user |> Notification.pending_participation_notification(event, total) |> Mailer.deliver_later() + + :ok else err -> require Logger