From 1087e19ee56442c4ee4b1db273c112bbf6846250 Mon Sep 17 00:00:00 2001
From: Thomas Citharel <tcit@tcit.fr>
Date: Tue, 27 Sep 2022 10:50:14 +0200
Subject: [PATCH] Enable E2E tests in CI

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
---
 .gitlab-ci.yml                              |  46 +--
 config/e2e.exs                              |  48 ++-
 js/.gitignore                               |   2 -
 js/README.md                                |  41 ---
 js/cypress.json                             |   7 -
 js/playwright.config.ts                     |   4 +-
 js/src/App.vue                              |  20 +-
 js/src/components/NavBar.vue                |   4 +-
 js/src/composition/apollo/user.ts           |  16 +-
 js/src/router/user.ts                       |   2 +-
 js/src/utils/identity.ts                    |   2 +-
 js/src/views/Account/RegisterView.vue       | 314 ++++++++++----------
 js/src/views/HomeView.vue                   |   7 +-
 js/src/views/User/RegisterView.vue          |   2 +-
 js/tests/e2e/login.spec.ts                  | 103 ++++++-
 lib/federation/activity_pub/types/actors.ex |   1 +
 lib/graphql/resolvers/admin.ex              |   3 +-
 lib/graphql/schema/search.ex                |  20 +-
 lib/mobilizon/events/events.ex              |  11 +-
 priv/repo/e2e.seed.exs                      |   2 +-
 20 files changed, 374 insertions(+), 281 deletions(-)
 delete mode 100644 js/README.md
 delete mode 100644 js/cypress.json

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 739e569f5..adee20efe 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -144,26 +144,32 @@ vitest:
         - js/junit.xml
     expire_in: 30 days
 
-# cypress:
-#   stage: test
-#   services:
-#     - name: postgis/postgis:13.3
-#       alias: postgres
-#   variables:
-#     MIX_ENV=e2e
-#   script:
-#     - mix ecto.create
-#     - mix ecto.migrate
-#     - mix run priv/repo/e2e.seed.exs
-#     - mix phx.server &
-#     - cd js
-#     - npx wait-on http://localhost:4000
-#     - if [ -z "$CYPRESS_KEY" ]; then npx cypress run; else npx cypress run --record --parallel --key $CYPRESS_KEY; fi
-#   artifacts:
-#     expire_in: 2 day
-#     paths:
-#       - js/tests/e2e/screenshots/**/*.png
-#       - js/tests/e2e/videos/**/*.mp4
+e2e:
+  stage: test
+  services:
+    - name: postgis/postgis:14-3.2
+      alias: postgres
+  variables:
+    MIX_ENV: "e2e"
+  before_script:
+    - mix deps.get
+    - mix ecto.create
+    - mix ecto.migrate
+    - mix run priv/repo/e2e.seed.exs
+    - cd js && yarn run build && npx playwright install && cd ../
+    - mix phx.digest
+  script:
+    - mix phx.server &
+    - cd js
+    - npx wait-on http://localhost:4000
+    - npx playwright test --project $BROWSER
+  parallel:
+    matrix:
+      - BROWSER: ['firefox', 'chromium']
+  artifacts:
+    expire_in: 2 days
+    paths:
+      - js/playwright-report
 
 pages:
   stage: deploy
diff --git a/config/e2e.exs b/config/e2e.exs
index cbb64310a..7135ea44b 100644
--- a/config/e2e.exs
+++ b/config/e2e.exs
@@ -19,19 +19,39 @@ config :mobilizon, Mobilizon.Web.Endpoint,
     yarn: [cd: Path.expand("../js", __DIR__)]
   ]
 
-require Logger
+config :vite_phx,
+  release_app: :mobilizon,
+  # Hard code :prod as an environment as :e2e will not be recongnized
+  environment: :prod,
+  vite_manifest: "priv/static/manifest.json",
+  phx_manifest: "priv/static/cache_manifest.json",
+  dev_server_address: "http://localhost:5173"
 
-cond do
-  System.get_env("INSTANCE_CONFIG") &&
-      File.exists?("./config/#{System.get_env("INSTANCE_CONFIG")}") ->
-    import_config System.get_env("INSTANCE_CONFIG")
+config :mobilizon, :instance,
+  name: "E2E Testing instance",
+  description: "E2E is safety",
+  hostname: "mobilizon1.com",
+  registrations_open: true,
+  registration_email_denylist: ["gmail.com", "deny@tcit.fr"],
+  demo: false,
+  default_language: "en",
+  allow_relay: true,
+  federating: true,
+  email_from: "mobilizon@mobilizon1.com",
+  email_reply_to: nil,
+  enable_instance_feeds: true,
+  koena_connect_link: true,
+  extra_categories: [
+    %{
+      id: :something_else,
+      label: "Quelque chose d'autre"
+    }
+  ]
 
-  System.get_env("DOCKER", "false") == "false" && File.exists?("./config/e2e.secret.exs") ->
-    import_config "e2e.secret.exs"
-
-  System.get_env("DOCKER", "false") == "true" ->
-    Logger.info("Using environment configuration for Docker")
-
-  true ->
-    Logger.error("No configuration file found")
-end
+config :mobilizon, Mobilizon.Storage.Repo,
+  adapter: Ecto.Adapters.Postgres,
+  username: System.get_env("MOBILIZON_DATABASE_USERNAME", "mobilizon_e2e"),
+  password: System.get_env("MOBILIZON_DATABASE_PASSWORD", "mobilizon_e2e"),
+  database: System.get_env("MOBILIZON_DATABASE_DBNAME", "mobilizon_e2e"),
+  hostname: System.get_env("MOBILIZON_DATABASE_HOST", "localhost"),
+  port: System.get_env("MOBILIZON_DATABASE_PORT") || "5432"
diff --git a/js/.gitignore b/js/.gitignore
index 5c5176c48..11369d996 100644
--- a/js/.gitignore
+++ b/js/.gitignore
@@ -2,8 +2,6 @@
 node_modules
 /dist
 
-/tests/e2e/videos/
-/tests/e2e/screenshots/
 /coverage
 stats.html
 
diff --git a/js/README.md b/js/README.md
deleted file mode 100644
index e4cb29dab..000000000
--- a/js/README.md
+++ /dev/null
@@ -1,41 +0,0 @@
-# mobilizon
-
-## Project setup
-
-```
-yarn install
-```
-
-### Compiles and hot-reloads for development
-
-```
-yarn serve
-```
-
-### Compiles and minifies for production
-
-```
-yarn build
-```
-
-### Run your unit tests
-
-```
-yarn test:unit
-```
-
-### Run your end-to-end tests
-
-```
-yarn test:e2e
-```
-
-### Lints and fixes files
-
-```
-yarn lint
-```
-
-### Customize configuration
-
-See [Configuration Reference](https://cli.vuejs.org/config/).
diff --git a/js/cypress.json b/js/cypress.json
deleted file mode 100644
index 17b7cd741..000000000
--- a/js/cypress.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
-  "pluginsFile": "tests/e2e/plugins/index.js",
-  "projectId": "86dpkx",
-  "baseUrl": "http://localhost:4000",
-  "viewportWidth": 1920,
-  "viewportHeight": 1080
-}
diff --git a/js/playwright.config.ts b/js/playwright.config.ts
index c3da6c26e..03e0bf459 100644
--- a/js/playwright.config.ts
+++ b/js/playwright.config.ts
@@ -13,7 +13,7 @@ import { devices } from "@playwright/test";
 const config: PlaywrightTestConfig = {
   testDir: "./tests/e2e",
   /* Maximum time one test can run for. */
-  timeout: 30 * 1000,
+  timeout: 10 * 1000,
   expect: {
     /**
      * Maximum time expect() should wait for the condition to be met.
@@ -36,7 +36,7 @@ const config: PlaywrightTestConfig = {
     /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
     actionTimeout: 0,
     /* Base URL to use in actions like `await page.goto('/')`. */
-    baseURL: "http://localhost:4005",
+    baseURL: "http://localhost:4000",
 
     /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
     trace: "on-first-retry",
diff --git a/js/src/App.vue b/js/src/App.vue
index 175d22d41..b27d71ea3 100644
--- a/js/src/App.vue
+++ b/js/src/App.vue
@@ -57,13 +57,17 @@ import {
 } from "vue";
 import { LocationType } from "@/types/user-location.model";
 import { useMutation, useQuery } from "@vue/apollo-composable";
-import { initializeCurrentActor } from "@/utils/identity";
+import {
+  initializeCurrentActor,
+  NoIdentitiesException,
+} from "@/utils/identity";
 import { useI18n } from "vue-i18n";
 import { Snackbar } from "@/plugins/snackbar";
 import { Notifier } from "@/plugins/notifier";
 import { CONFIG } from "@/graphql/config";
 import { IConfig } from "@/types/config.model";
 import { useRouter } from "vue-router";
+import RouteName from "@/router/name";
 
 const { result: configResult } = useQuery<{ config: IConfig }>(CONFIG);
 
@@ -130,7 +134,19 @@ interval.value = setInterval(async () => {
 
 onBeforeMount(async () => {
   if (initializeCurrentUser()) {
-    await initializeCurrentActor();
+    try {
+      await initializeCurrentActor();
+    } catch (err) {
+      if (err instanceof NoIdentitiesException) {
+        await router.push({
+          name: RouteName.REGISTER_PROFILE,
+          params: {
+            email: localStorage.getItem(AUTH_USER_EMAIL),
+            userAlreadyActivated: "true",
+          },
+        });
+      }
+    }
   }
 });
 
diff --git a/js/src/components/NavBar.vue b/js/src/components/NavBar.vue
index 8a85f7bd3..73752e447 100644
--- a/js/src/components/NavBar.vue
+++ b/js/src/components/NavBar.vue
@@ -406,11 +406,11 @@ watch(identities, () => {
   // If we don't have any identities, the user has validated their account,
   // is logging for the first time but didn't create an identity somehow
   if (identities.value && identities.value.length === 0) {
-    console.debug(
+    console.warn(
       "We have no identities listed for current user",
       identities.value
     );
-    console.debug("Pushing route to REGISTER_PROFILE");
+    console.info("Pushing route to REGISTER_PROFILE");
     router.push({
       name: RouteName.REGISTER_PROFILE,
       params: {
diff --git a/js/src/composition/apollo/user.ts b/js/src/composition/apollo/user.ts
index daa130a23..b285d7f7c 100644
--- a/js/src/composition/apollo/user.ts
+++ b/js/src/composition/apollo/user.ts
@@ -66,15 +66,7 @@ export async function updateLocale(locale: string) {
   }));
 }
 
-export function registerAccount(
-  variables: {
-    preferredUsername: string;
-    name: string;
-    summary: string;
-    email: string;
-  },
-  userAlreadyActivated: boolean
-) {
+export function registerAccount() {
   return useMutation<
     { registerPerson: IPerson },
     {
@@ -84,12 +76,12 @@ export function registerAccount(
       email: string;
     }
   >(REGISTER_PERSON, () => ({
-    variables,
     update: (
       store: ApolloCache<{ registerPerson: IPerson }>,
-      { data: localData }: FetchResult
+      { data: localData }: FetchResult,
+      { context }
     ) => {
-      if (userAlreadyActivated) {
+      if (context?.userAlreadyActivated) {
         const identitiesData = store.readQuery<{ identities: IPerson[] }>({
           query: IDENTITIES,
         });
diff --git a/js/src/router/user.ts b/js/src/router/user.ts
index 978cb9c83..85a1dd96b 100644
--- a/js/src/router/user.ts
+++ b/js/src/router/user.ts
@@ -28,7 +28,7 @@ export const userRoutes: RouteRecordRaw[] = [
     beforeEnter: beforeRegisterGuard,
   },
   {
-    path: "/register/profile",
+    path: "/register/profile/:email/:userAlreadyActivated?",
     name: UserRouteName.REGISTER_PROFILE,
     component: (): Promise<any> => import("@/views/Account/RegisterView.vue"),
     // We can only pass string values through params, therefore
diff --git a/js/src/utils/identity.ts b/js/src/utils/identity.ts
index fd3e61313..e4541ef51 100644
--- a/js/src/utils/identity.ts
+++ b/js/src/utils/identity.ts
@@ -11,7 +11,7 @@ import { computed, watch } from "vue";
 
 export class NoIdentitiesException extends Error {}
 
-export function saveActorData(obj: IPerson): void {
+function saveActorData(obj: IPerson): void {
   localStorage.setItem(AUTH_USER_ACTOR_ID, `${obj.id}`);
 }
 
diff --git a/js/src/views/Account/RegisterView.vue b/js/src/views/Account/RegisterView.vue
index 8b52b6cab..f2ec0af46 100644
--- a/js/src/views/Account/RegisterView.vue
+++ b/js/src/views/Account/RegisterView.vue
@@ -1,120 +1,127 @@
 <template>
-  <section class="container mx-auto">
-    <div class="">
-      <div class="">
-        <h1 class="text-2xl" v-if="userAlreadyActivated">
-          {{ $t("Congratulations, your account is now created!") }}
-        </h1>
-        <h1 class="text-2xl" v-else>
+  <section class="container mx-auto max-w-screen-sm">
+    <h1 class="text-2xl" v-if="userAlreadyActivated">
+      {{ t("Congratulations, your account is now created!") }}
+    </h1>
+    <h1 class="text-2xl" v-else>
+      {{
+        t("Register an account on {instanceName}!", {
+          instanceName,
+        })
+      }}
+    </h1>
+    <p class="prose dark:prose-invert" v-if="userAlreadyActivated">
+      {{ t("Now, create your first profile:") }}
+    </p>
+    <form v-if="!validationSent" @submit.prevent="submit">
+      <o-notification variant="danger" v-if="errors.extra">
+        {{ errors.extra }}
+      </o-notification>
+
+      <o-field :label="t('Displayed nickname')" labelFor="identityName">
+        <o-input
+          aria-required="true"
+          required
+          v-model="identity.name"
+          id="identityName"
+          @input="autoUpdateUsername"
+        />
+      </o-field>
+
+      <o-field
+        :label="t('Username')"
+        :variant="errors.preferred_username ? 'danger' : null"
+        :message="errors.preferred_username"
+        labelFor="identityPreferredUsername"
+      >
+        <o-field
+          :message="
+            t(
+              'Only alphanumeric lowercased characters and underscores are supported.'
+            )
+          "
+        >
+          <o-input
+            aria-required="true"
+            required
+            expanded
+            id="identityPreferredUsername"
+            v-model="identity.preferredUsername"
+            :validation-message="
+              identity.preferredUsername
+                ? t(
+                    'Only alphanumeric lowercased characters and underscores are supported.'
+                  )
+                : null
+            "
+          />
+          <p class="control">
+            <span class="button">@{{ host }}</span>
+          </p>
+        </o-field>
+      </o-field>
+      <p class="prose dark:prose-invert">
+        {{
+          t(
+            "This identifier is unique to your profile. It allows others to find you."
+          )
+        }}
+      </p>
+
+      <o-field :label="t('Short bio')" labelFor="identitySummary">
+        <o-input
+          type="textarea"
+          maxlength="100"
+          rows="2"
+          id="identitySummary"
+          v-model="identity.summary"
+        />
+      </o-field>
+
+      <p class="prose dark:prose-invert">
+        {{
+          t(
+            "You will be able to add an avatar and set other options in your account settings."
+          )
+        }}
+      </p>
+
+      <p class="text-center">
+        <o-button
+          variant="primary"
+          size="large"
+          native-type="submit"
+          :disabled="sendingValidation"
+          >{{ t("Create my profile") }}</o-button
+        >
+      </p>
+    </form>
+
+    <div v-if="validationSent && !userAlreadyActivated">
+      <o-notification variant="success" :closable="false">
+        <h2 class="title">
           {{
-            $t("Register an account on {instanceName}!", {
-              instanceName,
+            t("Your account is nearly ready, {username}", {
+              username: identity.name ?? identity.preferredUsername,
             })
           }}
-        </h1>
-        <p class="prose dark:prose-invert" v-if="userAlreadyActivated">
-          {{ $t("Now, create your first profile:") }}
+        </h2>
+        <i18n-t keypath="A validation email was sent to {email}" tag="p">
+          <template #email>
+            <code>{{ email }}</code>
+          </template>
+        </i18n-t>
+        <p>
+          {{
+            t(
+              "Before you can login, you need to click on the link inside it to validate your account."
+            )
+          }}
         </p>
-        <form v-if="!validationSent" @submit.prevent="submit">
-          <o-field :label="$t('Displayed nickname')">
-            <o-input
-              aria-required="true"
-              required
-              v-model="identity.name"
-              @input="autoUpdateUsername"
-            />
-          </o-field>
-
-          <o-field
-            :label="$t('Username')"
-            :type="errors.preferred_username ? 'is-danger' : null"
-            :message="errors.preferred_username"
-          >
-            <o-field
-              :message="
-                $t(
-                  'Only alphanumeric lowercased characters and underscores are supported.'
-                )
-              "
-            >
-              <o-input
-                aria-required="true"
-                required
-                expanded
-                v-model="identity.preferredUsername"
-                :validation-message="
-                  identity.preferredUsername
-                    ? $t(
-                        'Only alphanumeric lowercased characters and underscores are supported.'
-                      )
-                    : null
-                "
-              />
-              <p class="control">
-                <span class="button is-static">@{{ host }}</span>
-              </p>
-            </o-field>
-          </o-field>
-          <p class="description">
-            {{
-              $t(
-                "This identifier is unique to your profile. It allows others to find you."
-              )
-            }}
-          </p>
-
-          <o-field :label="$t('Short bio')">
-            <o-input
-              type="textarea"
-              maxlength="100"
-              rows="2"
-              v-model="identity.summary"
-            />
-          </o-field>
-
-          <p class="prose dark:prose-invert">
-            {{
-              $t(
-                "You will be able to add an avatar and set other options in your account settings."
-              )
-            }}
-          </p>
-
-          <p class="control has-text-centered">
-            <o-button
-              variant="primary"
-              size="large"
-              native-type="submit"
-              :disabled="sendingValidation"
-              >{{ $t("Create my profile") }}</o-button
-            >
-          </p>
-        </form>
-
-        <div v-if="validationSent && !userAlreadyActivated">
-          <o-notification variant="success" :closable="false">
-            <h2 class="title">
-              {{
-                $t("Your account is nearly ready, {username}", {
-                  username: identity.name ?? identity.preferredUsername,
-                })
-              }}
-            </h2>
-            <i18n-t keypath="A validation email was sent to {email}" tag="p">
-              <template #email>
-                <code>{{ email }}</code>
-              </template>
-            </i18n-t>
-            <p>
-              {{
-                $t(
-                  "Before you can login, you need to click on the link inside it to validate your account."
-                )
-              }}
-            </p>
-          </o-notification>
-        </div>
-      </div>
+        <o-button tag="router-link" :to="{ name: RouteName.HOME }">{{
+          t("Back to homepage")
+        }}</o-button>
+      </o-notification>
     </div>
   </section>
 </template>
@@ -173,61 +180,42 @@ const autoUpdateUsername = () => {
   identity.value.preferredUsername = convertToUsername(identity.value.name);
 };
 
+const { onDone, onError, mutate } = registerAccount();
+
+onDone(async ({ data }) => {
+  validationSent.value = true;
+  window.localStorage.setItem("new-registered-user", "yes");
+
+  if (data && props.userAlreadyActivated) {
+    await changeIdentity(data.registerPerson);
+
+    await router.push({ name: RouteName.HOME });
+  }
+});
+
+onError((err) => {
+  errors.value = err.graphQLErrors.reduce(
+    (acc: { [key: string]: string }, error: any) => {
+      acc[error.details ?? error.field ?? "extra"] = Array.isArray(
+        error.message
+      )
+        ? (error.message as string[]).join(",")
+        : error.message;
+      return acc;
+    },
+    {}
+  );
+  console.error("Error while registering person", err);
+  console.error("Errors while registering person", errors);
+  sendingValidation.value = false;
+});
+
 const submit = async (): Promise<void> => {
   sendingValidation.value = true;
   errors.value = {};
-  const { onDone, onError } = registerAccount(
+  mutate(
     { email: props.email, ...identity.value },
-    props.userAlreadyActivated
+    { context: { userAlreadyActivated: props.userAlreadyActivated } }
   );
-
-  onDone(async ({ data }) => {
-    validationSent.value = true;
-    window.localStorage.setItem("new-registered-user", "yes");
-
-    if (data && props.userAlreadyActivated) {
-      await changeIdentity(data.registerPerson);
-
-      await router.push({ name: RouteName.HOME });
-    }
-  });
-
-  onError((err) => {
-    errors.value = err.graphQLErrors.reduce(
-      (acc: { [key: string]: string }, error: any) => {
-        acc[error.details || error.field] = error.message;
-        return acc;
-      },
-      {}
-    );
-    console.error("Error while registering person", err);
-    console.error("Errors while registering person", errors);
-    sendingValidation.value = false;
-  });
 };
 </script>
-
-<style lang="scss" scoped>
-.avatar-enter-active {
-  transition: opacity 1s ease;
-}
-
-.avatar-enter,
-.avatar-leave-to {
-  opacity: 0;
-}
-
-.avatar-leave {
-  display: none;
-}
-
-.container .columns {
-  margin: 1rem auto 3rem;
-}
-
-p.description {
-  font-size: 0.9rem;
-  margin-bottom: 15px;
-  margin-top: -10px;
-}
-</style>
diff --git a/js/src/views/HomeView.vue b/js/src/views/HomeView.vue
index 544b27d3e..2aeee3e2d 100644
--- a/js/src/views/HomeView.vue
+++ b/js/src/views/HomeView.vue
@@ -365,7 +365,12 @@ onMounted(() => {
 const router = useRouter();
 
 watch(loggedUser, (loggedUserValue) => {
-  if (loggedUserValue?.id && loggedUserValue?.settings === null) {
+  if (
+    loggedUserValue?.id &&
+    loggedUserValue?.settings === null &&
+    loggedUserValue.defaultActor?.id
+  ) {
+    console.info("No user settings, going to onboarding", loggedUserValue);
     router.push({
       name: RouteName.WELCOME_SCREEN,
       params: { step: "1" },
diff --git a/js/src/views/User/RegisterView.vue b/js/src/views/User/RegisterView.vue
index 625480c32..685dce4fa 100644
--- a/js/src/views/User/RegisterView.vue
+++ b/js/src/views/User/RegisterView.vue
@@ -91,7 +91,7 @@
         <form @submit.prevent="submit">
           <o-field
             :label="t('Email')"
-            :type="errorEmailType"
+            :variant="errorEmailType"
             :message="errorEmailMessage"
             label-for="email"
           >
diff --git a/js/tests/e2e/login.spec.ts b/js/tests/e2e/login.spec.ts
index f35f84faa..f7f11dc6a 100644
--- a/js/tests/e2e/login.spec.ts
+++ b/js/tests/e2e/login.spec.ts
@@ -59,8 +59,8 @@ test("Login rejects unknown users properly", async ({ page }) => {
 test("Tries to login with valid credentials", async ({ page, context }) => {
   await page.goto("/login");
 
-  await page.locator("#email").fill("user@provider.org");
-  await page.locator("#password").fill("valid_passw0rd");
+  await page.locator("#email").fill("user@email.com");
+  await page.locator("#password").fill("some password");
 
   const loginButton = page.locator("form button", { hasText: "Login" });
 
@@ -69,5 +69,102 @@ test("Tries to login with valid credentials", async ({ page, context }) => {
   await loginButton.click();
   await page.waitForURL("/");
   expect(new URL(page.url()).pathname).toBe("/");
-  expect((await context.storageState()).origins[0].localStorage).toBe("toto");
+  const localStorage = (
+    await context.storageState()
+  ).origins[0].localStorage.reduce((acc: Record<string, string>, elem) => {
+    acc[elem.name] = elem.value;
+    return acc;
+  }, {});
+  expect(localStorage["auth-user-role"]).toBe("USER");
+  expect(localStorage["auth-access-token"]).toBeDefined();
+  expect(localStorage["auth-refresh-token"]).toBeDefined();
+  expect(localStorage["auth-user-email"]).toBe("user@email.com");
+  // Changes each run
+  // expect(localStorage["auth-user-id"]).toBe("3");
+  // Doesn't work in Chrome for some reason
+  // expect(localStorage['auth-user-actor-id']).toBe('2');
+});
+
+test("Tries to login with valid credentials but unconfirmed account", async ({
+  page,
+}) => {
+  await page.goto("/login");
+  await page.locator("#email").fill("unconfirmed@email.com");
+  await page.locator("#password").fill("some password");
+  await page.keyboard.press("Enter");
+
+  await expect(page.locator(".notification-danger")).toHaveText(
+    "User not found"
+  );
+});
+
+test("Tries to login with valid credentials, confirmed account but no profile", async ({
+  page,
+}) => {
+  await page.goto("/login");
+  await page.locator("#email").fill("confirmed@email.com");
+  await page.locator("#password").fill("some password");
+  await page.keyboard.press("Enter");
+
+  await page.waitForURL("/register/profile/confirmed@email.com/true");
+  expect(page.url()).toContain("/register/profile/confirmed@email.com/true");
+
+  await expect(page.locator("p.prose").first()).toHaveText(
+    "Now, create your first profile:"
+  );
+
+  const displayNameField = page.locator("form > .field").first();
+  await expect(displayNameField.locator("label")).toHaveText(
+    "Displayed nickname"
+  );
+  const displayNameInput = displayNameField.locator("input");
+  await displayNameInput.fill("Duplicate");
+
+  const usernameField = page.locator("form > .field").nth(1);
+  await expect(usernameField.locator("label")).toHaveText("Username");
+  const usernameFieldInput = usernameField.locator("input");
+  await usernameFieldInput.fill("test_user");
+
+  const descriptionField = page.locator("form > .field").nth(2);
+  await expect(descriptionField.locator("label")).toHaveText("Short bio");
+  await descriptionField
+    .locator("textarea")
+    .fill("This shouln't work because it's using a dupublicated username");
+
+  const submitButton = page.locator('button[type="submit"]', {
+    hasText: "Create my profile",
+  });
+  await submitButton.click();
+
+  await expect(page.locator("p.field-message-danger")).toHaveText(
+    "This username is already taken."
+  );
+
+  await displayNameInput.fill("");
+  await displayNameInput.fill("Not");
+
+  await usernameFieldInput.fill("");
+  await usernameFieldInput.fill("test_user_2");
+
+  await submitButton.click();
+
+  // 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.get(".navbar-link span.icon i").should(
+  //   "have.class",
+  //   "mdi-account-circle"
+  // );
+
+  await page.waitForURL("/");
+  expect(page.url()).toContain("/");
+
+  await expect(page.locator(".notification-info")).toHaveText(
+    "Welcome to Mobilizon, Not!"
+  );
+  await expect(
+    page.locator("button#user-menu-button span:not(.sr-only)")
+  ).toHaveClass("material-design-icon account-circle-icon");
 });
diff --git a/lib/federation/activity_pub/types/actors.ex b/lib/federation/activity_pub/types/actors.ex
index e612000c7..f62ca43f6 100644
--- a/lib/federation/activity_pub/types/actors.ex
+++ b/lib/federation/activity_pub/types/actors.ex
@@ -210,6 +210,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
         if actor_type == :Application do
           Instances.refresh()
         end
+
         FollowMailer.send_notification_to_admins(follower)
         follower_as_data = Convertible.model_to_as(follower)
         approve_if_manually_approves_followers(follower, follower_as_data)
diff --git a/lib/graphql/resolvers/admin.ex b/lib/graphql/resolvers/admin.ex
index e89fc132c..bdb35c98c 100644
--- a/lib/graphql/resolvers/admin.ex
+++ b/lib/graphql/resolvers/admin.ex
@@ -467,7 +467,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
   end
 
   @spec get_instance(any, map(), Absinthe.Resolution.t()) ::
-          {:error, :unauthenticated | :unauthorized | :not_found} | {:ok, Mobilizon.Instances.Instance.t()}
+          {:error, :unauthenticated | :unauthorized | :not_found}
+          | {:ok, Mobilizon.Instances.Instance.t()}
   def get_instance(_parent, %{domain: domain}, %{
         context: %{current_user: %User{role: role}}
       })
diff --git a/lib/graphql/schema/search.ex b/lib/graphql/schema/search.ex
index 1fcaeca1f..2cc36e509 100644
--- a/lib/graphql/schema/search.ex
+++ b/lib/graphql/schema/search.ex
@@ -24,7 +24,10 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
     field(:tags, list_of(:tag), description: "The event's tags")
     field(:category, :event_category, description: "The event's category")
     field(:options, :event_options, description: "The event options")
-    field(:participant_stats, :participant_stats, description: "Statistics on the event's participants")
+
+    field(:participant_stats, :participant_stats,
+      description: "Statistics on the event's participants"
+    )
 
     resolve_type(fn
       %Event{}, _ ->
@@ -55,7 +58,10 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
     field(:tags, list_of(:tag), description: "The event's tags")
     field(:category, :event_category, description: "The event's category")
     field(:options, :event_options, description: "The event options")
-    field(:participant_stats, :participant_stats, description: "Statistics on the event's participants")
+
+    field(:participant_stats, :participant_stats,
+      description: "Statistics on the event's participants"
+    )
   end
 
   interface :group_search_result do
@@ -198,7 +204,10 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
       arg(:language_one_of, list_of(:string),
         description: "The list of languages this event can be in"
       )
-      arg(:boost_languages, list_of(:string), description: "The user's languages that can benefit from a boost in search results")
+
+      arg(:boost_languages, list_of(:string),
+        description: "The user's languages that can benefit from a boost in search results"
+      )
 
       arg(:search_target, :search_target,
         default_value: :internal,
@@ -238,7 +247,10 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
       arg(:language_one_of, list_of(:string),
         description: "The list of languages this event can be in"
       )
-      arg(:boost_languages, list_of(:string), description: "The user's languages that can benefit from a boost in search results")
+
+      arg(:boost_languages, list_of(:string),
+        description: "The user's languages that can benefit from a boost in search results"
+      )
 
       arg(:search_target, :search_target,
         default_value: :internal,
diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex
index e09822e7c..3890b1031 100644
--- a/lib/mobilizon/events/events.ex
+++ b/lib/mobilizon/events/events.ex
@@ -541,7 +541,7 @@ defmodule Mobilizon.Events do
     |> filter_draft()
     |> filter_local_or_from_followed_instances_events()
     |> filter_public_visibility()
-    |> event_order(args.sort_by)
+    |> event_order(Map.get(args, :sort_by, :match_desc))
     |> Page.build_page(page, limit, :begins_on)
   end
 
@@ -1819,11 +1819,16 @@ defmodule Mobilizon.Events do
     |> event_order_begins_on_asc()
   end
 
-  defp event_order(query, :match_desc), do: order_by(query, [e, f], desc: f.rank, asc: e.begins_on)
+  defp event_order(query, :match_desc),
+    do: order_by(query, [e, f], desc: f.rank, asc: e.begins_on)
+
   defp event_order(query, :start_time_desc), do: order_by(query, [e], asc: e.begins_on)
   defp event_order(query, :created_at_desc), do: order_by(query, [e], desc: e.publish_at)
   defp event_order(query, :created_at_asc), do: order_by(query, [e], asc: e.publish_at)
-  defp event_order(query, :participant_count_desc), do: order_by(query, [e], fragment("participant_stats->>'participant' DESC"))
+
+  defp event_order(query, :participant_count_desc),
+    do: order_by(query, [e], fragment("participant_stats->>'participant' DESC"))
+
   defp event_order(query, _), do: query
 
   defp event_order_begins_on_asc(query),
diff --git a/priv/repo/e2e.seed.exs b/priv/repo/e2e.seed.exs
index fba8c31c2..39cc2e847 100644
--- a/priv/repo/e2e.seed.exs
+++ b/priv/repo/e2e.seed.exs
@@ -3,7 +3,7 @@ defmodule EndToEndSeed do
 
   def delete_user(email) do
     with {:ok, user} <- Users.get_user_by_email(email) do
-      Users.delete_user(user)
+      Users.delete_user(user, reserve_email: false)
     end
   end
 end