From 5e75daa73282c2c356f79e4d26722bd5ba6181d7 Mon Sep 17 00:00:00 2001 From: Thomas Citharel <tcit@tcit.fr> Date: Sat, 12 Oct 2019 13:16:36 +0200 Subject: [PATCH] Add e2e seed and test event creation Signed-off-by: Thomas Citharel <tcit@tcit.fr> --- .gitlab-ci.yml | 1 + config/config.exs | 3 +- js/src/components/Event/EventFullDate.vue | 2 +- js/src/components/NavBar.vue | 4 +- js/src/i18n/en_US.json | 3 +- js/src/i18n/fr_FR.json | 3 +- js/src/utils/auth.ts | 7 ++- js/src/views/Account/Register.vue | 11 +++-- js/src/views/User/Login.vue | 13 +++++- js/tests/e2e/specs/dashboard.js | 8 ---- js/tests/e2e/specs/event.js | 44 +++++++++++++++++ js/tests/e2e/specs/login.js | 48 +++++++++++++++++-- js/tests/e2e/specs/register.js | 6 +-- js/tests/e2e/support/commands.js | 56 ++++++++++++++++++++++ lib/mobilizon_web/router.ex | 1 + priv/repo/e2e.seed.exs | 57 +++++++++++++++++++++++ 16 files changed, 237 insertions(+), 30 deletions(-) create mode 100644 js/tests/e2e/specs/event.js create mode 100644 priv/repo/e2e.seed.exs diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a983eab5c..a18f19bf9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -91,6 +91,7 @@ cypress: - cd ../ - MIX_ENV=e2e mix ecto.create - MIX_ENV=e2e mix ecto.migrate + - MIX_ENV=e2e mix run priv/repo/e2e.seed.exs - MIX_ENV=e2e mix phx.server & - cd js - npx wait-on http://localhost:4000 diff --git a/config/config.exs b/config/config.exs index 6da132e77..d911236fd 100644 --- a/config/config.exs +++ b/config/config.exs @@ -7,7 +7,8 @@ import Config # General application configuration config :mobilizon, - ecto_repos: [Mobilizon.Storage.Repo] + ecto_repos: [Mobilizon.Storage.Repo], + env: Mix.env() config :mobilizon, :instance, name: System.get_env("MOBILIZON_INSTANCE_NAME") || "My Mobilizon Instance", diff --git a/js/src/components/Event/EventFullDate.vue b/js/src/components/Event/EventFullDate.vue index b8076d8d3..15bc0f47d 100644 --- a/js/src/components/Event/EventFullDate.vue +++ b/js/src/components/Event/EventFullDate.vue @@ -20,7 +20,7 @@ <template> <span v-if="!endsOn">{{ beginsOn | formatDateTimeString }}</span> <span v-else-if="isSameDay()"> - {{ $t('The {date} from {startTime} to {endTime}', {date: formatDate(beginsOn), startTime: formatTime(beginsOn), endTime: formatTime(endsOn)}) }} + {{ $t('On {date} from {startTime} to {endTime}', {date: formatDate(beginsOn), startTime: formatTime(beginsOn), endTime: formatTime(endsOn)}) }} </span> <span v-else-if="endsOn"> {{ $t('From the {startDate} at {startTime} to the {endDate} at {endTime}', diff --git a/js/src/components/NavBar.vue b/js/src/components/NavBar.vue index 35df42b2b..fc367b6ef 100644 --- a/js/src/components/NavBar.vue +++ b/js/src/components/NavBar.vue @@ -137,8 +137,8 @@ export default class NavBar extends Vue { } } - async handleErrors(errors: GraphQLError) { - if (errors[0].message === 'You need to be logged-in to view your list of identities') { + async handleErrors(errors: GraphQLError[]) { + if (errors.length > 0 && errors[0].message === 'You need to be logged-in to view your list of identities') { await this.logout(); } } diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json index 4ba78e570..26e30f91a 100644 --- a/js/src/i18n/en_US.json +++ b/js/src/i18n/en_US.json @@ -143,6 +143,7 @@ "No results for \"{queryText}\"": "No results for \"{queryText}\"", "Number of places": "Number of places", "Old password": "Old password", + "On {date} from {startTime} to {endTime}": "On {date} from {startTime} to {endTime}", "One person is going": "No one is going | One person is going | {approved} persons are going", "Only accessible through link and search (private)": "Only accessible through link and search (private)", "Opened reports": "Opened reports", @@ -216,13 +217,13 @@ "The page you're looking for doesn't exist.": "The page you're looking for doesn't exist.", "The password was successfully changed": "The password was successfully changed", "The report will be sent to the moderators of your instance. You can explain why you report this content below.": "The report will be sent to the moderators of your instance. You can explain why you report this content below.", - "The {date} from {startTime} to {endTime}": "The {date} from {startTime} to {endTime}", "These events may interest you": "These events may interest you", "This installation (called “instance“) can easily {interconnect}, thanks to {protocol}.": "This installation (called “instance“) can easily {interconnect}, thanks to {protocol}.", "This instance isn't opened to registrations, but you can register on other instances.": "This instance isn't opened to registrations, but you can register on other instances.", "This is a demonstration site to test the beta version of Mobilizon.": "This is a demonstration site to test the beta version of Mobilizon.", "This will delete / anonymize all content (events, comments, messages, participations…) created from this identity.": "This will delete / anonymize all content (events, comments, messages, participations…) created from this identity.", "Title": "Title", + "To achieve your registration, please create a first identity profile.": "To achieve your registration, please create a first identity profile.", "To change the world, change the software": "To change the world, change the software", "To confirm, type your identity username \"{preferredUsername}\"": "To confirm, type your identity username \"{preferredUsername}\"", "Transfer to {outsideDomain}": "Transfer to {outsideDomain}", diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json index 855144503..f133d8c14 100644 --- a/js/src/i18n/fr_FR.json +++ b/js/src/i18n/fr_FR.json @@ -168,6 +168,7 @@ "No results for \"{queryText}\"": "Pas de résultats pour « {queryText} »", "Number of places": "Nombre de places", "Old password": "Ancien mot de passe", + "On {date} from {startTime} to {endTime}": "On {date} de {startTime} à {endTime}", "One person is going": "Personne n'y va | Une personne y va | {approved} personnes y vont", "Only accessible through link and search (private)": "Uniquement accessibles par lien et la recherche (privé)", "Opened reports": "Signalements ouverts", @@ -248,7 +249,6 @@ "The password was successfully changed": "Le mot de passe a été changé avec succès", "The report will be sent to the moderators of your instance. You can explain why you report this content below.": "Le signalement sera envoyé aux modérateur⋅ices de votre instance. Vous pouvez expliquer pourquoi vous signalez ce contenu ci-dessous.", "The {date} at {time}": "Le {date} à {time}", - "The {date} from {startTime} to {endTime}": "Le {date} de {startTime} à {endTime}", "There are {participants} participants.": "Il n'y a qu'un⋅e participant⋅e. | Il y a {participants} participants.", "These events may interest you": "Ces événements peuvent vous intéresser", "This installation (called “instance“) can easily {interconnect}, thanks to {protocol}.": "Cette installation (appelée “instance“) peut facilement {interconnect}, grâce à {protocol}.", @@ -256,6 +256,7 @@ "This is a demonstration site to test the beta version of Mobilizon.": "Ceci est un site de démonstration permettant de tester la version bêta de Mobilizon.", "This will delete / anonymize all content (events, comments, messages, participations…) created from this identity.": "Cela supprimera / anonymisera tout le contenu (événements, commentaires, messages, participations…) créés avec cette identité.", "Title": "Titre", + "To achieve your registration, please create a first identity profile.": "Pour finir votre inscription, veuillez créer un premier profil.", "To change the world, change the software": "Changer de logiciel pour changer le monde", "To confirm, type your event title \"{eventTitle}\"": "Pour confirmer, entrez le titre de l'événement « {eventTitle} »", "To confirm, type your identity username \"{preferredUsername}\"": "Pour confirmer, entrez le nom de l’identité « {preferredUsername} »", diff --git a/js/src/utils/auth.ts b/js/src/utils/auth.ts index a1901c6ec..6acf28281 100644 --- a/js/src/utils/auth.ts +++ b/js/src/utils/auth.ts @@ -37,6 +37,8 @@ export function deleteUserData() { } } +export class NoIdentitiesException extends Error {} + /** * We fetch from localStorage the latest actor ID used, * then fetch the current identities to set in cache @@ -50,7 +52,10 @@ export async function initializeCurrentActor(apollo: ApolloClient<any>) { fetchPolicy: 'network-only', }); const identities = result.data.identities; - if (identities.length < 1) return; + if (identities.length < 1) { + console.warn('Logged user has no identities!'); + throw new NoIdentitiesException; + } const activeIdentity = identities.find(identity => identity.id === actorId) || identities[0] as IPerson; if (activeIdentity) { diff --git a/js/src/views/Account/Register.vue b/js/src/views/Account/Register.vue index c6840ee7f..e8d34a0f8 100644 --- a/js/src/views/Account/Register.vue +++ b/js/src/views/Account/Register.vue @@ -5,7 +5,10 @@ <h1 class="title"> {{ $t('Register an account on Mobilizon!') }} </h1> - <form v-if="!validationSent"> + <b-message v-if="userAlreadyActivated"> + {{ $t('To achieve your registration, please create a first identity profile.')}} + </b-message> + <form v-if="!validationSent" @submit.prevent="submit"> <b-field :label="$t('Username')" :type="errors.preferred_username ? 'is-danger' : null" @@ -33,7 +36,7 @@ </b-field> <p class="control has-text-centered"> - <b-button type="is-primary" size="is-large" @click="submit()"> + <b-button type="is-primary" size="is-large" native-type="submit"> {{ $t('Create my profile') }} </b-button> </p> @@ -117,8 +120,8 @@ export default class Register extends Vue { acc[error.details] = error.message; return acc; }, {}); - console.error(error); - console.error(this.errors); + console.error('Error while registering person', error); + console.error('Errors while registering person', this.errors); } } } diff --git a/js/src/views/User/Login.vue b/js/src/views/User/Login.vue index edb147f1c..e89a89d9c 100644 --- a/js/src/views/User/Login.vue +++ b/js/src/views/User/Login.vue @@ -62,7 +62,7 @@ import { Component, Prop, Vue } from 'vue-property-decorator'; import { LOGIN } from '@/graphql/auth'; import { validateEmailField, validateRequiredField } from '@/utils/validators'; -import { initializeCurrentActor, saveUserData } from '@/utils/auth'; +import { initializeCurrentActor, NoIdentitiesException, saveUserData } from '@/utils/auth'; import { ILogin } from '@/types/login.model'; import { CURRENT_USER_CLIENT, UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user'; import { onLogin } from '@/vue-apollo'; @@ -153,7 +153,16 @@ export default class Login extends Vue { role: data.login.user.role, }, }); - await initializeCurrentActor(this.$apollo.provider.defaultClient); + try { + await initializeCurrentActor(this.$apollo.provider.defaultClient); + } catch (e) { + if (e instanceof NoIdentitiesException) { + return await this.$router.push({ + name: RouteName.REGISTER_PROFILE, + params: { email: this.currentUser.email, userAlreadyActivated: 'true' }, + }); + } + } onLogin(this.$apollo); diff --git a/js/tests/e2e/specs/dashboard.js b/js/tests/e2e/specs/dashboard.js index 6027d9bb3..8d738429b 100644 --- a/js/tests/e2e/specs/dashboard.js +++ b/js/tests/e2e/specs/dashboard.js @@ -1,14 +1,6 @@ // https://docs.cypress.io/api/introduction/api.html import { onBeforeLoad } from './browser-language'; -beforeEach(() => { - cy.restoreLocalStorage(); -}); - -afterEach(() => { - cy.saveLocalStorage(); -}); - describe('Homepage', () => { it('Checks the footer', () => { cy.visit('/', { onBeforeLoad }); diff --git a/js/tests/e2e/specs/event.js b/js/tests/e2e/specs/event.js new file mode 100644 index 000000000..cc1780e1a --- /dev/null +++ b/js/tests/e2e/specs/event.js @@ -0,0 +1,44 @@ +import { onBeforeLoad } from './browser-language'; + +beforeEach(() => { + cy.clearLocalStorage(); + cy.checkoutSession(); +}); + +afterEach(() => { + cy.dropSession(); +}); + +describe('Events', () => { + it('Shows my current events', () => { + const EVENT = { title: 'My first event'}; + + cy.loginUser(); + cy.visit('/events/me', { onBeforeLoad }); + cy.contains('.message.is-danger', 'No events found'); + cy.contains('.navbar-item', 'Create').click(); + + cy.url().should('include', 'create'); + cy.get('.field').first().find('input').type(EVENT.title); + cy.get('.field').eq(1).find('input').type('my tag, holo{enter}'); + cy.get('.field').eq(2).find('.datepicker .dropdown-trigger').click(); + + cy.get('.field').eq(3).find('.pagination-list .control').first().find('.select select').select('September'); + cy.get('.field').eq(3).find('.pagination-list .control').last().find('.select select').select('2021'); + cy.wait(1000); + cy.get('.field').eq(3).contains('.datepicker-cell', '15').click(); + + cy.contains('.button.is-primary', 'Create my event').click(); + cy.url().should('include', '/events/'); + cy.contains('.title', EVENT.title); + cy.contains('.title-and-informations span small', 'You\'re the only one going to this event'); + cy.contains('.date-and-privacy', 'On Wednesday, September 15, 2021 from'); + cy.contains('.visibility .tag', 'Public event'); + + cy.contains('.navbar-item', 'My events').click(); + cy.contains('.title', EVENT.title); + cy.contains('.content.column', 'You\'re organizing this event'); + cy.contains('.title-wrapper .date-component .datetime-container .month', 'Sep'); + cy.contains('.title-wrapper .date-component .datetime-container .day', '15'); + }); +}); \ No newline at end of file diff --git a/js/tests/e2e/specs/login.js b/js/tests/e2e/specs/login.js index 4e4c5c78e..7df635a87 100644 --- a/js/tests/e2e/specs/login.js +++ b/js/tests/e2e/specs/login.js @@ -1,11 +1,7 @@ import { onBeforeLoad } from './browser-language'; beforeEach(() => { - cy.restoreLocalStorage(); -}); - -afterEach(() => { - cy.saveLocalStorage(); + cy.clearLocalStorage(); }); describe('Login', () => { @@ -42,4 +38,46 @@ describe('Login', () => { cy.contains('.message.is-danger', 'User with email not found'); }); + + it('Tries to login with valid credentials', () => { + cy.visit('/login', { onBeforeLoad }); + cy.get('input[type=email]').type('user@email.com'); + cy.get('input[type=password]').type('some password'); + cy.get('form').submit(); + cy.contains('.navbar-link', 'test_user'); + cy.contains('article.message.is-info', 'Welcome back I\'m a test user'); + cy.contains('.navbar-item.has-dropdown', 'test_user').click(); + cy.get('.navbar-item').last().contains('Log out').click(); + }); + + it('Tries to login with valid credentials but unconfirmed account', () => { + cy.visit('/login', { onBeforeLoad }); + cy.get('input[type=email]').type('unconfirmed@email.com'); + cy.get('input[type=password]').type('some password'); + cy.get('form').submit(); + cy.contains('.message.is-danger', 'User with email not found'); + }); + + it('Tries to login with valid credentials, confirmed account but no profile', () => { + cy.visit('/login', { onBeforeLoad }); + cy.get('input[type=email]').type('confirmed@email.com'); + cy.get('input[type=password]').type('some password'); + cy.get('form').submit(); + + cy.contains('.message', 'To achieve your registration, please create a first identity profile.'); + cy.get('form .field').first().contains('label', 'Username').parent().find('input').type('test_user'); + cy.get('form .field').eq(2).contains('label', 'Displayed name').parent().find('input').type('Duplicate'); + cy.get('form .field').eq(3).contains('label', 'Description').parent().find('textarea').type('This shouln\'t work because it\' using a dupublicated username'); + cy.get('.control.has-text-centered').contains('button', 'Create my profile').click(); + cy.contains('.help.is-danger', 'Username is already taken'); + + cy.get('form .field input').first(0).clear().type('test_user_2'); + cy.get('form .field input').eq(1).type('Not'); + cy.get('form .field textarea').clear().type('This will now work'); + cy.get('form').submit(); + cy.wait(1000); + + cy.contains('.navbar-link', 'test_user_2'); + cy.contains('article.message.is-info', 'Welcome back DuplicateNot'); + }); }); diff --git a/js/tests/e2e/specs/register.js b/js/tests/e2e/specs/register.js index 9cc3aa1b2..e34ee3bf9 100644 --- a/js/tests/e2e/specs/register.js +++ b/js/tests/e2e/specs/register.js @@ -1,12 +1,10 @@ import { onBeforeLoad } from './browser-language'; beforeEach(() => { - cy.restoreLocalStorage(); cy.checkoutSession(); }); afterEach(() => { - cy.saveLocalStorage(); cy.dropSession(); }); @@ -34,7 +32,7 @@ describe('Registration', () => { it('Tests that registration works', () => { cy.visit('/register/user', { onBeforeLoad }); - cy.get('input[type=email]').type('user@email.com'); + cy.get('input[type=email]').type('user2register@email.com'); cy.get('input[type=password]').type('userPassword'); cy.get('form').contains('button.button.is-primary', 'Register').click(); @@ -45,7 +43,7 @@ describe('Registration', () => { cy.get('form .field').eq(3).contains('label', 'Description').parent().find('textarea').type('This is a test account'); cy.get('.control.has-text-centered').contains('button', 'Create my profile').click(); - cy.contains('article.message.is-success', 'Your account is nearly ready, tester').contains('A validation email was sent to user@email.com'); + cy.contains('article.message.is-success', 'Your account is nearly ready, tester').contains('A validation email was sent to user2register@email.com'); cy.visit('/sent_emails'); diff --git a/js/tests/e2e/support/commands.js b/js/tests/e2e/support/commands.js index f6ec60cac..45826ba9f 100644 --- a/js/tests/e2e/support/commands.js +++ b/js/tests/e2e/support/commands.js @@ -24,6 +24,14 @@ // -- This is will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) +const AUTH_ACCESS_TOKEN = 'auth-access-token'; +const AUTH_REFRESH_TOKEN = 'auth-refresh-token'; +const AUTH_USER_ID = 'auth-user-id'; +const AUTH_USER_EMAIL = 'auth-user-email'; +const AUTH_USER_ACTOR_ID = 'auth-user-actor-id'; +const AUTH_USER_ROLE = 'auth-user-role'; + + let LOCAL_STORAGE_MEMORY = {}; Cypress.Commands.add("saveLocalStorage", () => { @@ -38,6 +46,54 @@ Cypress.Commands.add("restoreLocalStorage", () => { }); }); +Cypress.Commands.add("clearLocalStorage", () => { + Object.keys(LOCAL_STORAGE_MEMORY).forEach(key => { + localStorage.removeItem(key); + }); +}); + +Cypress.Commands.add("loginUser", () => { + const loginMutation = ` + mutation Login($email: String!, $password: String!) { + login(email: $email, password: $password) { + accessToken, + refreshToken, + user { + id, + email, + role + } + }, +}`; + + const body = JSON.stringify({ + operationName: 'Login', + query: loginMutation, + variables: { email: 'user@email.com', password: 'some password' } + }); + + cy.request({ + url: 'http://localhost:4000/api', + body: body, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }).then((res) => { + console.log(res); + const obj = res.body.data.login; + console.log(obj); + + localStorage.setItem(AUTH_USER_ID, `${obj.user.id}`); + localStorage.setItem(AUTH_USER_EMAIL, obj.user.email); + localStorage.setItem(AUTH_USER_ROLE, obj.user.role); + + localStorage.setItem(AUTH_USER_ACTOR_ID, `${obj.id}`); + localStorage.setItem(AUTH_ACCESS_TOKEN, obj.accessToken); + localStorage.setItem(AUTH_REFRESH_TOKEN, obj.refreshToken); + }); + }); + Cypress.Commands.add('checkoutSession', async () => { const response = await fetch('/sandbox', { cache: 'no-store', diff --git a/lib/mobilizon_web/router.ex b/lib/mobilizon_web/router.ex index b73e2e5b3..7bd1bf983 100644 --- a/lib/mobilizon_web/router.ex +++ b/lib/mobilizon_web/router.ex @@ -77,6 +77,7 @@ defmodule MobilizonWeb.Router do # Because the "/events/:uuid" route caches all these, we need to force them get("/events/create", PageController, :index) get("/events/list", PageController, :index) + get("/events/me", PageController, :index) get("/events/:uuid/edit", PageController, :index) # This is a hack to ease link generation into emails diff --git a/priv/repo/e2e.seed.exs b/priv/repo/e2e.seed.exs new file mode 100644 index 000000000..fba8c31c2 --- /dev/null +++ b/priv/repo/e2e.seed.exs @@ -0,0 +1,57 @@ +defmodule EndToEndSeed do + alias Mobilizon.Users + + def delete_user(email) do + with {:ok, user} <- Users.get_user_by_email(email) do + Users.delete_user(user) + end + end +end + +alias Mobilizon.Users +alias Mobilizon.Users.User +alias Mobilizon.Actors +alias Mobilizon.Actors.Actor + +if Application.get_env(:mobilizon, :env) != :e2e do + exit(:shutdown) +end + +# An user that has just been registered +test_user_unconfirmed = %{email: "unconfirmed@email.com", password: "some password"} + +# An user that has registered and has confirmed their account, but no attached identity +test_user_confirmed = %{email: "confirmed@email.com", password: "some password"} + +# An user that has registered and has confirmed their account, with a profile +test_user = %{email: "user@email.com", password: "some password"} +test_actor = %{preferred_username: "test_user", name: "I'm a test user", domain: nil} + +EndToEndSeed.delete_user(test_user_unconfirmed.email) +EndToEndSeed.delete_user(test_user_confirmed.email) +EndToEndSeed.delete_user(test_user.email) + +{:ok, %User{} = _user_unconfirmed} = Users.register(test_user_unconfirmed) + +{:ok, %User{} = user_confirmed} = Users.register(test_user_confirmed) + +Users.update_user(user_confirmed, %{ + "confirmed_at" => Timex.shift(user_confirmed.confirmation_sent_at, hours: -3), + "confirmation_sent_at" => nil, + "confirmation_token" => nil +}) + +{:ok, %User{} = user} = Users.register(test_user) + +Users.update_user(user, %{ + "confirmed_at" => Timex.shift(user.confirmation_sent_at, hours: -3), + "confirmation_sent_at" => nil, + "confirmation_token" => nil +}) + +{:ok, %Actor{}} = + Actors.new_person(%{ + user_id: user.id, + preferred_username: test_actor.preferred_username, + name: test_actor.name + })