Enable E2E tests in CI

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2022-09-27 10:50:14 +02:00
parent 680f812bdf
commit 1087e19ee5
No known key found for this signature in database
GPG key ID: A061B9DDE0CA0773
20 changed files with 374 additions and 281 deletions

View file

@ -144,26 +144,32 @@ vitest:
- js/junit.xml - js/junit.xml
expire_in: 30 days expire_in: 30 days
# cypress: e2e:
# stage: test stage: test
# services: services:
# - name: postgis/postgis:13.3 - name: postgis/postgis:14-3.2
# alias: postgres alias: postgres
# variables: variables:
# MIX_ENV=e2e MIX_ENV: "e2e"
# script: before_script:
# - mix ecto.create - mix deps.get
# - mix ecto.migrate - mix ecto.create
# - mix run priv/repo/e2e.seed.exs - mix ecto.migrate
# - mix phx.server & - mix run priv/repo/e2e.seed.exs
# - cd js - cd js && yarn run build && npx playwright install && cd ../
# - npx wait-on http://localhost:4000 - mix phx.digest
# - if [ -z "$CYPRESS_KEY" ]; then npx cypress run; else npx cypress run --record --parallel --key $CYPRESS_KEY; fi script:
# artifacts: - mix phx.server &
# expire_in: 2 day - cd js
# paths: - npx wait-on http://localhost:4000
# - js/tests/e2e/screenshots/**/*.png - npx playwright test --project $BROWSER
# - js/tests/e2e/videos/**/*.mp4 parallel:
matrix:
- BROWSER: ['firefox', 'chromium']
artifacts:
expire_in: 2 days
paths:
- js/playwright-report
pages: pages:
stage: deploy stage: deploy

View file

@ -19,19 +19,39 @@ config :mobilizon, Mobilizon.Web.Endpoint,
yarn: [cd: Path.expand("../js", __DIR__)] 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 config :mobilizon, :instance,
System.get_env("INSTANCE_CONFIG") && name: "E2E Testing instance",
File.exists?("./config/#{System.get_env("INSTANCE_CONFIG")}") -> description: "E2E is safety",
import_config System.get_env("INSTANCE_CONFIG") 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") -> config :mobilizon, Mobilizon.Storage.Repo,
import_config "e2e.secret.exs" adapter: Ecto.Adapters.Postgres,
username: System.get_env("MOBILIZON_DATABASE_USERNAME", "mobilizon_e2e"),
System.get_env("DOCKER", "false") == "true" -> password: System.get_env("MOBILIZON_DATABASE_PASSWORD", "mobilizon_e2e"),
Logger.info("Using environment configuration for Docker") database: System.get_env("MOBILIZON_DATABASE_DBNAME", "mobilizon_e2e"),
hostname: System.get_env("MOBILIZON_DATABASE_HOST", "localhost"),
true -> port: System.get_env("MOBILIZON_DATABASE_PORT") || "5432"
Logger.error("No configuration file found")
end

2
js/.gitignore vendored
View file

@ -2,8 +2,6 @@
node_modules node_modules
/dist /dist
/tests/e2e/videos/
/tests/e2e/screenshots/
/coverage /coverage
stats.html stats.html

View file

@ -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/).

View file

@ -1,7 +0,0 @@
{
"pluginsFile": "tests/e2e/plugins/index.js",
"projectId": "86dpkx",
"baseUrl": "http://localhost:4000",
"viewportWidth": 1920,
"viewportHeight": 1080
}

View file

@ -13,7 +13,7 @@ import { devices } from "@playwright/test";
const config: PlaywrightTestConfig = { const config: PlaywrightTestConfig = {
testDir: "./tests/e2e", testDir: "./tests/e2e",
/* Maximum time one test can run for. */ /* Maximum time one test can run for. */
timeout: 30 * 1000, timeout: 10 * 1000,
expect: { expect: {
/** /**
* Maximum time expect() should wait for the condition to be met. * 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). */ /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0, actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */ /* 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 */ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry", trace: "on-first-retry",

View file

@ -57,13 +57,17 @@ import {
} from "vue"; } from "vue";
import { LocationType } from "@/types/user-location.model"; import { LocationType } from "@/types/user-location.model";
import { useMutation, useQuery } from "@vue/apollo-composable"; import { useMutation, useQuery } from "@vue/apollo-composable";
import { initializeCurrentActor } from "@/utils/identity"; import {
initializeCurrentActor,
NoIdentitiesException,
} from "@/utils/identity";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { Snackbar } from "@/plugins/snackbar"; import { Snackbar } from "@/plugins/snackbar";
import { Notifier } from "@/plugins/notifier"; import { Notifier } from "@/plugins/notifier";
import { CONFIG } from "@/graphql/config"; import { CONFIG } from "@/graphql/config";
import { IConfig } from "@/types/config.model"; import { IConfig } from "@/types/config.model";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import RouteName from "@/router/name";
const { result: configResult } = useQuery<{ config: IConfig }>(CONFIG); const { result: configResult } = useQuery<{ config: IConfig }>(CONFIG);
@ -130,7 +134,19 @@ interval.value = setInterval(async () => {
onBeforeMount(async () => { onBeforeMount(async () => {
if (initializeCurrentUser()) { if (initializeCurrentUser()) {
try {
await initializeCurrentActor(); await initializeCurrentActor();
} catch (err) {
if (err instanceof NoIdentitiesException) {
await router.push({
name: RouteName.REGISTER_PROFILE,
params: {
email: localStorage.getItem(AUTH_USER_EMAIL),
userAlreadyActivated: "true",
},
});
}
}
} }
}); });

View file

@ -406,11 +406,11 @@ watch(identities, () => {
// If we don't have any identities, the user has validated their account, // 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 // is logging for the first time but didn't create an identity somehow
if (identities.value && identities.value.length === 0) { if (identities.value && identities.value.length === 0) {
console.debug( console.warn(
"We have no identities listed for current user", "We have no identities listed for current user",
identities.value identities.value
); );
console.debug("Pushing route to REGISTER_PROFILE"); console.info("Pushing route to REGISTER_PROFILE");
router.push({ router.push({
name: RouteName.REGISTER_PROFILE, name: RouteName.REGISTER_PROFILE,
params: { params: {

View file

@ -66,15 +66,7 @@ export async function updateLocale(locale: string) {
})); }));
} }
export function registerAccount( export function registerAccount() {
variables: {
preferredUsername: string;
name: string;
summary: string;
email: string;
},
userAlreadyActivated: boolean
) {
return useMutation< return useMutation<
{ registerPerson: IPerson }, { registerPerson: IPerson },
{ {
@ -84,12 +76,12 @@ export function registerAccount(
email: string; email: string;
} }
>(REGISTER_PERSON, () => ({ >(REGISTER_PERSON, () => ({
variables,
update: ( update: (
store: ApolloCache<{ registerPerson: IPerson }>, store: ApolloCache<{ registerPerson: IPerson }>,
{ data: localData }: FetchResult { data: localData }: FetchResult,
{ context }
) => { ) => {
if (userAlreadyActivated) { if (context?.userAlreadyActivated) {
const identitiesData = store.readQuery<{ identities: IPerson[] }>({ const identitiesData = store.readQuery<{ identities: IPerson[] }>({
query: IDENTITIES, query: IDENTITIES,
}); });

View file

@ -28,7 +28,7 @@ export const userRoutes: RouteRecordRaw[] = [
beforeEnter: beforeRegisterGuard, beforeEnter: beforeRegisterGuard,
}, },
{ {
path: "/register/profile", path: "/register/profile/:email/:userAlreadyActivated?",
name: UserRouteName.REGISTER_PROFILE, name: UserRouteName.REGISTER_PROFILE,
component: (): Promise<any> => import("@/views/Account/RegisterView.vue"), component: (): Promise<any> => import("@/views/Account/RegisterView.vue"),
// We can only pass string values through params, therefore // We can only pass string values through params, therefore

View file

@ -11,7 +11,7 @@ import { computed, watch } from "vue";
export class NoIdentitiesException extends Error {} export class NoIdentitiesException extends Error {}
export function saveActorData(obj: IPerson): void { function saveActorData(obj: IPerson): void {
localStorage.setItem(AUTH_USER_ACTOR_ID, `${obj.id}`); localStorage.setItem(AUTH_USER_ACTOR_ID, `${obj.id}`);
} }

View file

@ -1,38 +1,42 @@
<template> <template>
<section class="container mx-auto"> <section class="container mx-auto max-w-screen-sm">
<div class="">
<div class="">
<h1 class="text-2xl" v-if="userAlreadyActivated"> <h1 class="text-2xl" v-if="userAlreadyActivated">
{{ $t("Congratulations, your account is now created!") }} {{ t("Congratulations, your account is now created!") }}
</h1> </h1>
<h1 class="text-2xl" v-else> <h1 class="text-2xl" v-else>
{{ {{
$t("Register an account on {instanceName}!", { t("Register an account on {instanceName}!", {
instanceName, instanceName,
}) })
}} }}
</h1> </h1>
<p class="prose dark:prose-invert" v-if="userAlreadyActivated"> <p class="prose dark:prose-invert" v-if="userAlreadyActivated">
{{ $t("Now, create your first profile:") }} {{ t("Now, create your first profile:") }}
</p> </p>
<form v-if="!validationSent" @submit.prevent="submit"> <form v-if="!validationSent" @submit.prevent="submit">
<o-field :label="$t('Displayed nickname')"> <o-notification variant="danger" v-if="errors.extra">
{{ errors.extra }}
</o-notification>
<o-field :label="t('Displayed nickname')" labelFor="identityName">
<o-input <o-input
aria-required="true" aria-required="true"
required required
v-model="identity.name" v-model="identity.name"
id="identityName"
@input="autoUpdateUsername" @input="autoUpdateUsername"
/> />
</o-field> </o-field>
<o-field <o-field
:label="$t('Username')" :label="t('Username')"
:type="errors.preferred_username ? 'is-danger' : null" :variant="errors.preferred_username ? 'danger' : null"
:message="errors.preferred_username" :message="errors.preferred_username"
labelFor="identityPreferredUsername"
> >
<o-field <o-field
:message=" :message="
$t( t(
'Only alphanumeric lowercased characters and underscores are supported.' 'Only alphanumeric lowercased characters and underscores are supported.'
) )
" "
@ -41,52 +45,54 @@
aria-required="true" aria-required="true"
required required
expanded expanded
id="identityPreferredUsername"
v-model="identity.preferredUsername" v-model="identity.preferredUsername"
:validation-message=" :validation-message="
identity.preferredUsername identity.preferredUsername
? $t( ? t(
'Only alphanumeric lowercased characters and underscores are supported.' 'Only alphanumeric lowercased characters and underscores are supported.'
) )
: null : null
" "
/> />
<p class="control"> <p class="control">
<span class="button is-static">@{{ host }}</span> <span class="button">@{{ host }}</span>
</p> </p>
</o-field> </o-field>
</o-field> </o-field>
<p class="description"> <p class="prose dark:prose-invert">
{{ {{
$t( t(
"This identifier is unique to your profile. It allows others to find you." "This identifier is unique to your profile. It allows others to find you."
) )
}} }}
</p> </p>
<o-field :label="$t('Short bio')"> <o-field :label="t('Short bio')" labelFor="identitySummary">
<o-input <o-input
type="textarea" type="textarea"
maxlength="100" maxlength="100"
rows="2" rows="2"
id="identitySummary"
v-model="identity.summary" v-model="identity.summary"
/> />
</o-field> </o-field>
<p class="prose dark:prose-invert"> <p class="prose dark:prose-invert">
{{ {{
$t( t(
"You will be able to add an avatar and set other options in your account settings." "You will be able to add an avatar and set other options in your account settings."
) )
}} }}
</p> </p>
<p class="control has-text-centered"> <p class="text-center">
<o-button <o-button
variant="primary" variant="primary"
size="large" size="large"
native-type="submit" native-type="submit"
:disabled="sendingValidation" :disabled="sendingValidation"
>{{ $t("Create my profile") }}</o-button >{{ t("Create my profile") }}</o-button
> >
</p> </p>
</form> </form>
@ -95,7 +101,7 @@
<o-notification variant="success" :closable="false"> <o-notification variant="success" :closable="false">
<h2 class="title"> <h2 class="title">
{{ {{
$t("Your account is nearly ready, {username}", { t("Your account is nearly ready, {username}", {
username: identity.name ?? identity.preferredUsername, username: identity.name ?? identity.preferredUsername,
}) })
}} }}
@ -107,15 +113,16 @@
</i18n-t> </i18n-t>
<p> <p>
{{ {{
$t( t(
"Before you can login, you need to click on the link inside it to validate your account." "Before you can login, you need to click on the link inside it to validate your account."
) )
}} }}
</p> </p>
<o-button tag="router-link" :to="{ name: RouteName.HOME }">{{
t("Back to homepage")
}}</o-button>
</o-notification> </o-notification>
</div> </div>
</div>
</div>
</section> </section>
</template> </template>
@ -173,13 +180,7 @@ const autoUpdateUsername = () => {
identity.value.preferredUsername = convertToUsername(identity.value.name); identity.value.preferredUsername = convertToUsername(identity.value.name);
}; };
const submit = async (): Promise<void> => { const { onDone, onError, mutate } = registerAccount();
sendingValidation.value = true;
errors.value = {};
const { onDone, onError } = registerAccount(
{ email: props.email, ...identity.value },
props.userAlreadyActivated
);
onDone(async ({ data }) => { onDone(async ({ data }) => {
validationSent.value = true; validationSent.value = true;
@ -195,7 +196,11 @@ const submit = async (): Promise<void> => {
onError((err) => { onError((err) => {
errors.value = err.graphQLErrors.reduce( errors.value = err.graphQLErrors.reduce(
(acc: { [key: string]: string }, error: any) => { (acc: { [key: string]: string }, error: any) => {
acc[error.details || error.field] = error.message; acc[error.details ?? error.field ?? "extra"] = Array.isArray(
error.message
)
? (error.message as string[]).join(",")
: error.message;
return acc; return acc;
}, },
{} {}
@ -204,30 +209,13 @@ const submit = async (): Promise<void> => {
console.error("Errors while registering person", errors); console.error("Errors while registering person", errors);
sendingValidation.value = false; sendingValidation.value = false;
}); });
const submit = async (): Promise<void> => {
sendingValidation.value = true;
errors.value = {};
mutate(
{ email: props.email, ...identity.value },
{ context: { userAlreadyActivated: props.userAlreadyActivated } }
);
}; };
</script> </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>

View file

@ -365,7 +365,12 @@ onMounted(() => {
const router = useRouter(); const router = useRouter();
watch(loggedUser, (loggedUserValue) => { 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({ router.push({
name: RouteName.WELCOME_SCREEN, name: RouteName.WELCOME_SCREEN,
params: { step: "1" }, params: { step: "1" },

View file

@ -91,7 +91,7 @@
<form @submit.prevent="submit"> <form @submit.prevent="submit">
<o-field <o-field
:label="t('Email')" :label="t('Email')"
:type="errorEmailType" :variant="errorEmailType"
:message="errorEmailMessage" :message="errorEmailMessage"
label-for="email" label-for="email"
> >

View file

@ -59,8 +59,8 @@ test("Login rejects unknown users properly", async ({ page }) => {
test("Tries to login with valid credentials", async ({ page, context }) => { test("Tries to login with valid credentials", async ({ page, context }) => {
await page.goto("/login"); await page.goto("/login");
await page.locator("#email").fill("user@provider.org"); await page.locator("#email").fill("user@email.com");
await page.locator("#password").fill("valid_passw0rd"); await page.locator("#password").fill("some password");
const loginButton = page.locator("form button", { hasText: "Login" }); 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 loginButton.click();
await page.waitForURL("/"); await page.waitForURL("/");
expect(new URL(page.url()).pathname).toBe("/"); 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");
}); });

View file

@ -210,6 +210,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
if actor_type == :Application do if actor_type == :Application do
Instances.refresh() Instances.refresh()
end end
FollowMailer.send_notification_to_admins(follower) FollowMailer.send_notification_to_admins(follower)
follower_as_data = Convertible.model_to_as(follower) follower_as_data = Convertible.model_to_as(follower)
approve_if_manually_approves_followers(follower, follower_as_data) approve_if_manually_approves_followers(follower, follower_as_data)

View file

@ -467,7 +467,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
end end
@spec get_instance(any, map(), Absinthe.Resolution.t()) :: @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}, %{ def get_instance(_parent, %{domain: domain}, %{
context: %{current_user: %User{role: role}} context: %{current_user: %User{role: role}}
}) })

View file

@ -24,7 +24,10 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
field(:tags, list_of(:tag), description: "The event's tags") field(:tags, list_of(:tag), description: "The event's tags")
field(:category, :event_category, description: "The event's category") field(:category, :event_category, description: "The event's category")
field(:options, :event_options, description: "The event options") 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 resolve_type(fn
%Event{}, _ -> %Event{}, _ ->
@ -55,7 +58,10 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
field(:tags, list_of(:tag), description: "The event's tags") field(:tags, list_of(:tag), description: "The event's tags")
field(:category, :event_category, description: "The event's category") field(:category, :event_category, description: "The event's category")
field(:options, :event_options, description: "The event options") 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 end
interface :group_search_result do interface :group_search_result do
@ -198,7 +204,10 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
arg(:language_one_of, list_of(:string), arg(:language_one_of, list_of(:string),
description: "The list of languages this event can be in" 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, arg(:search_target, :search_target,
default_value: :internal, default_value: :internal,
@ -238,7 +247,10 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
arg(:language_one_of, list_of(:string), arg(:language_one_of, list_of(:string),
description: "The list of languages this event can be in" 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, arg(:search_target, :search_target,
default_value: :internal, default_value: :internal,

View file

@ -541,7 +541,7 @@ defmodule Mobilizon.Events do
|> filter_draft() |> filter_draft()
|> filter_local_or_from_followed_instances_events() |> filter_local_or_from_followed_instances_events()
|> filter_public_visibility() |> filter_public_visibility()
|> event_order(args.sort_by) |> event_order(Map.get(args, :sort_by, :match_desc))
|> Page.build_page(page, limit, :begins_on) |> Page.build_page(page, limit, :begins_on)
end end
@ -1819,11 +1819,16 @@ defmodule Mobilizon.Events do
|> event_order_begins_on_asc() |> event_order_begins_on_asc()
end 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, :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_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, :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(query, _), do: query
defp event_order_begins_on_asc(query), defp event_order_begins_on_asc(query),

View file

@ -3,7 +3,7 @@ defmodule EndToEndSeed do
def delete_user(email) do def delete_user(email) do
with {:ok, user} <- Users.get_user_by_email(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 end
end end