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
+  })