Implement search with PostgreSQL trigrams

Signed-off-by: Thomas Citharel <tcit@tcit.fr>

Rename function to reflect that we only get one result

Signed-off-by: Thomas Citharel <tcit@tcit.fr>

Add loggers and make Ecto call parallels during search

Signed-off-by: Thomas Citharel <tcit@tcit.fr>

Implement trigrams for events & replace pg similarity operator % with <%

Signed-off-by: Thomas Citharel <tcit@tcit.fr>

Fix tests

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2019-02-21 18:11:49 +01:00
parent 131152abac
commit 4ec40d601b
No known key found for this signature in database
GPG key ID: A061B9DDE0CA0773
18 changed files with 422 additions and 141 deletions

View file

@ -467,6 +467,11 @@ defmodule Mobilizon.Actors do
|> Repo.preload(:organized_events) |> Repo.preload(:organized_events)
end end
@doc """
Getting an actor from url, eventually creating it
"""
# TODO: Move this to Mobilizon.Service.ActivityPub
@spec get_or_fetch_by_url(String.t(), bool()) :: {:ok, Actor.t()} | {:error, String.t()}
def get_or_fetch_by_url(url, preload \\ false) do def get_or_fetch_by_url(url, preload \\ false) do
with {:ok, actor} <- get_actor_by_url(url, preload) do with {:ok, actor} <- get_actor_by_url(url, preload) do
{:ok, actor} {:ok, actor}
@ -498,16 +503,32 @@ defmodule Mobilizon.Actors do
end end
@doc """ @doc """
Find local users by it's username Find local users by their username
""" """
# TODO: This doesn't seem to be used anyway
def find_local_by_username(username) do def find_local_by_username(username) do
actors = actors =
Repo.all( Repo.all(
from( from(
a in Actor, a in Actor,
where: where:
(ilike(a.preferred_username, ^like_sanitize(username)) or fragment(
ilike(a.name, ^like_sanitize(username))) and is_nil(a.domain) "f_unaccent(?) <% f_unaccent(?) or
f_unaccent(coalesce(?, '')) <% f_unaccent(?)",
a.preferred_username,
^username,
a.name,
^username
),
where: is_nil(a.domain),
order_by:
fragment(
"word_similarity(?, ?) + word_similarity(coalesce(?, ''), ?) desc",
a.preferred_username,
^username,
a.name,
^username
)
) )
) )
@ -526,48 +547,31 @@ defmodule Mobilizon.Actors do
from( from(
a in Actor, a in Actor,
where: where:
ilike(a.preferred_username, ^like_sanitize(username)) or fragment(
ilike(a.name, ^like_sanitize(username)) "f_unaccent(?) %> f_unaccent(?) or
f_unaccent(coalesce(?, '')) %> f_unaccent(?)",
a.preferred_username,
^username,
a.name,
^username
),
order_by:
fragment(
"word_similarity(?, ?) + word_similarity(coalesce(?, ''), ?) desc",
a.preferred_username,
^username,
a.name,
^username
)
) )
|> paginate(page, limit) |> paginate(page, limit)
) )
end end
# Sanitize the LIKE queries
defp like_sanitize(value) do
"%" <> String.replace(value, ~r/([\\%_])/, "\\1") <> "%"
end
@email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
@spec search(String.t()) :: {:ok, list(Actor.t())} | {:ok, []} | {:error, any()}
def search(name) do
# find already saved accounts
case find_actors_by_username_or_name(name) do
[] ->
# no accounts found, let's test if it's an username@domain.tld
with true <- Regex.match?(@email_regex, name),
# creating the actor in that case
{:ok, actor} <- ActivityPub.find_or_make_actor_from_nickname(name) do
{:ok, [actor]}
else
false ->
{:ok, []}
# error fingering the actor
{:error, err} ->
{:error, err}
end
actors = [_ | _] ->
# actors already saved found !
{:ok, actors}
end
end
@doc """ @doc """
Find a group by its actor id Get a group by its actor id
""" """
def find_group_by_actor_id(actor_id) do def get_group_by_actor_id(actor_id) do
case Repo.get_by(Actor, id: actor_id, type: :Group) do case Repo.get_by(Actor, id: actor_id, type: :Group) do
nil -> {:error, :group_not_found} nil -> {:error, :group_not_found}
actor -> {:ok, actor} actor -> {:ok, actor}

View file

@ -45,7 +45,7 @@ defmodule Mobilizon.Actors.User do
:password, :password,
min: 6, min: 6,
max: 100, max: 100,
message: "The choosen password is too short." message: "The chosen password is too short."
) )
if Map.has_key?(attrs, :default_actor) do if Map.has_key?(attrs, :default_actor) do

View file

@ -248,7 +248,19 @@ defmodule Mobilizon.Events do
query = query =
from(e in Event, from(e in Event,
where: e.visibility == ^:public and ilike(e.title, ^like_sanitize(name)), where:
e.visibility == ^:public and
fragment(
"f_unaccent(?) %> f_unaccent(?)",
e.title,
^name
),
order_by:
fragment(
"word_similarity(?, ?) desc",
e.title,
^name
),
preload: [:organizer_actor] preload: [:organizer_actor]
) )
|> paginate(page, limit) |> paginate(page, limit)
@ -256,11 +268,6 @@ defmodule Mobilizon.Events do
Repo.all(query) Repo.all(query)
end end
# Sanitize the LIKE queries
defp like_sanitize(value) do
"%" <> String.replace(value, ~r/([\\%_])/, "\\1") <> "%"
end
@doc """ @doc """
Creates a event. Creates a event.

View file

@ -0,0 +1,99 @@
defmodule MobilizonWeb.API.Search do
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Events
alias Mobilizon.Events.{Event, Comment}
require Logger
@doc """
Search
"""
@spec search(String.t(), integer(), integer()) ::
{:ok, list(Actor.t())} | {:ok, []} | {:error, any()}
def search(search, page \\ 1, limit \\ 10) do
do_search(search, page, limit, %{events: true, actors: true})
end
@doc """
Not used at the moment
"""
# TODO: Use me
@spec search_actors(String.t(), integer(), integer()) ::
{:ok, list(Actor.t())} | {:ok, []} | {:error, any()}
def search_actors(search, page \\ 1, limit \\ 10) do
do_search(search, page, limit, %{actors: true})
end
@doc """
Not used at the moment
"""
# TODO: Use me
@spec search_events(String.t(), integer(), integer()) ::
{:ok, list(Event.t())} | {:ok, []} | {:error, any()}
def search_events(search, page \\ 1, limit \\ 10) do
do_search(search, page, limit, %{events: true})
end
# Do the actual search
@spec do_search(String.t(), integer(), integer(), map()) :: {:ok, list(any())}
defp do_search(search, page, limit, opts) do
search = String.trim(search)
cond do
search == "" ->
{:error, "Search can't be empty"}
String.match?(search, ~r/@/) ->
{:ok, process_from_username(search)}
String.starts_with?(search, "https://") ->
{:ok, process_from_url(search)}
String.starts_with?(search, "http://") ->
{:ok, process_from_url(search)}
true ->
events =
Task.async(fn ->
if Map.get(opts, :events, false),
do: Events.find_events_by_name(search, page, limit),
else: []
end)
actors =
Task.async(fn ->
if Map.get(opts, :actors, false),
do: Actors.find_actors_by_username_or_name(search, page, limit),
else: []
end)
{:ok, Task.await(events) ++ Task.await(actors)}
end
end
# If the search string is an username
@spec process_from_username(String.t()) :: Actor.t() | nil
defp process_from_username(search) do
with {:ok, actor} <- ActivityPub.find_or_make_actor_from_nickname(search) do
actor
else
{:error, _err} ->
Logger.debug("Unable to find or make actor '#{search}'")
nil
end
end
# If the search string is an URL
@spec process_from_url(String.t()) :: Actor.t() | Event.t() | Comment.t() | nil
defp process_from_url(search) do
with {:ok, object} <- ActivityPub.fetch_object_from_url(search) do
object
else
{:error, _err} ->
Logger.debug("Unable to find or make object from URL '#{search}'")
nil
end
end
end

View file

@ -2,7 +2,6 @@ defmodule MobilizonWeb.Resolvers.Event do
@moduledoc """ @moduledoc """
Handles the event-related GraphQL calls Handles the event-related GraphQL calls
""" """
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Activity alias Mobilizon.Activity
alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Actors.User alias Mobilizon.Actors.User
@ -122,39 +121,6 @@ defmodule MobilizonWeb.Resolvers.Event do
{:error, "You need to be logged-in to leave an event"} {:error, "You need to be logged-in to leave an event"}
end end
@doc """
Search events by title
"""
def search_events(_parent, %{search: search, page: page, limit: limit}, _resolution) do
{:ok, Mobilizon.Events.find_events_by_name(search, page, limit)}
end
@doc """
Search events and actors by title
"""
def search_events_and_actors(_parent, %{search: search, page: page, limit: limit}, _resolution) do
search = String.trim(search)
found =
case String.contains?(search, "@") do
true ->
with {:ok, actor} <- ActivityPub.find_or_make_actor_from_nickname(search) do
actor
else
{:error, _err} ->
nil
end
_ ->
Mobilizon.Events.find_events_by_name(search, page, limit) ++
Mobilizon.Actors.find_actors_by_username_or_name(search, page, limit)
end
require Logger
Logger.debug(inspect(found))
{:ok, found}
end
@doc """ @doc """
Create an event Create an event
""" """

View file

@ -81,7 +81,7 @@ defmodule MobilizonWeb.Resolvers.Group do
} }
} }
) do ) do
with {:ok, %Actor{} = group} <- Actors.find_group_by_actor_id(group_id), with {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
{:is_owned, true, _} <- User.owns_actor(user, actor_id), {:is_owned, true, _} <- User.owns_actor(user, actor_id),
{:ok, %Member{} = member} <- Member.get_member(actor_id, group.id), {:ok, %Member{} = member} <- Member.get_member(actor_id, group.id),
{:is_admin, true} <- Member.is_administrator(member), {:is_admin, true} <- Member.is_administrator(member),

View file

@ -0,0 +1,13 @@
defmodule MobilizonWeb.Resolvers.Search do
@moduledoc """
Handles the event-related GraphQL calls
"""
alias MobilizonWeb.API.Search
@doc """
Search events and actors by title
"""
def search_events_and_actors(_parent, %{search: search, page: page, limit: limit}, _resolution) do
Search.search(search, page, limit)
end
end

View file

@ -122,7 +122,7 @@ defmodule MobilizonWeb.Schema do
arg(:search, non_null(:string)) arg(:search, non_null(:string))
arg(:page, :integer, default_value: 1) arg(:page, :integer, default_value: 1)
arg(:limit, :integer, default_value: 10) arg(:limit, :integer, default_value: 10)
resolve(&Resolvers.Event.search_events_and_actors/3) resolve(&Resolvers.Search.search_events_and_actors/3)
end end
import_fields(:user_queries) import_fields(:user_queries)

View file

@ -66,6 +66,7 @@ defmodule Mobilizon.Service.ActivityPub do
@doc """ @doc """
Fetch an object from an URL, from our local database of events and comments, then eventually remote Fetch an object from an URL, from our local database of events and comments, then eventually remote
""" """
# TODO: Make database calls parallel
@spec fetch_object_from_url(String.t()) :: {:ok, %Event{}} | {:ok, %Comment{}} | {:error, any()} @spec fetch_object_from_url(String.t()) :: {:ok, %Event{}} | {:ok, %Comment{}} | {:error, any()}
def fetch_object_from_url(url) do def fetch_object_from_url(url) do
Logger.info("Fetching object from url #{url}") Logger.info("Fetching object from url #{url}")
@ -73,6 +74,7 @@ defmodule Mobilizon.Service.ActivityPub do
with true <- String.starts_with?(url, "http"), with true <- String.starts_with?(url, "http"),
nil <- Events.get_event_by_url(url), nil <- Events.get_event_by_url(url),
nil <- Events.get_comment_from_url(url), nil <- Events.get_comment_from_url(url),
{:error, :actor_not_found} <- Actors.get_actor_by_url(url),
{:ok, %{body: body, status_code: code}} when code in 200..299 <- {:ok, %{body: body, status_code: code}} when code in 200..299 <-
HTTPoison.get( HTTPoison.get(
url, url,
@ -97,12 +99,16 @@ defmodule Mobilizon.Service.ActivityPub do
"Note" -> "Note" ->
{:ok, Events.get_comment_full_from_url!(activity.data["object"]["id"])} {:ok, Events.get_comment_full_from_url!(activity.data["object"]["id"])}
"Actor" ->
{:ok, Actors.get_actor_by_url!(activity.data["object"]["id"], true)}
other -> other ->
{:error, other} {:error, other}
end end
else else
%Event{url: event_url} -> {:ok, Events.get_event_by_url!(event_url)} %Event{url: event_url} -> {:ok, Events.get_event_by_url!(event_url)}
%Comment{url: comment_url} -> {:ok, Events.get_comment_full_from_url!(comment_url)} %Comment{url: comment_url} -> {:ok, Events.get_comment_full_from_url!(comment_url)}
%Actor{url: actor_url} -> {:ok, Actors.get_actor_by_url!(actor_url, true)}
e -> {:error, e} e -> {:error, e}
end end
end end

View file

@ -94,7 +94,8 @@ defmodule Mobilizon.Mixfile do
{:ex_unit_notifier, "~> 0.1", only: :test}, {:ex_unit_notifier, "~> 0.1", only: :test},
{:dialyxir, "~> 1.0.0-rc.4", only: [:dev], runtime: false}, {:dialyxir, "~> 1.0.0-rc.4", only: [:dev], runtime: false},
{:exvcr, "~> 0.10", only: :test}, {:exvcr, "~> 0.10", only: :test},
{:credo, "~> 1.0.0", only: [:dev, :test], runtime: false} {:credo, "~> 1.0.0", only: [:dev, :test], runtime: false},
{:mock, "~> 0.3.0", only: :test}
] ]
end end

View file

@ -64,6 +64,7 @@
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"},
"mix_test_watch": {:hex, :mix_test_watch, "0.9.0", "c72132a6071261893518fa08e121e911c9358713f62794a90c95db59042af375", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm"}, "mix_test_watch": {:hex, :mix_test_watch, "0.9.0", "c72132a6071261893518fa08e121e911c9358713f62794a90c95db59042af375", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm"},
"mmdb2_decoder": {:hex, :mmdb2_decoder, "0.3.0", "03a159a52342d3328cf2b774f1036e56719f7edc7f919180588a7764854c3318", [:mix], [], "hexpm"}, "mmdb2_decoder": {:hex, :mmdb2_decoder, "0.3.0", "03a159a52342d3328cf2b774f1036e56719f7edc7f919180588a7764854c3318", [:mix], [], "hexpm"},
"mock": {:hex, :mock, "0.3.3", "42a433794b1291a9cf1525c6d26b38e039e0d3a360732b5e467bfc77ef26c914", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"},
"nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"}, "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"},
"phoenix": {:hex, :phoenix, "1.4.1", "801f9d632808657f1f7c657c8bbe624caaf2ba91429123ebe3801598aea4c3d9", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"}, "phoenix": {:hex, :phoenix, "1.4.1", "801f9d632808657f1f7c657c8bbe624caaf2ba91429123ebe3801598aea4c3d9", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"},

View file

@ -2,6 +2,15 @@ defmodule Mobilizon.Repo.Migrations.Prerequites do
use Ecto.Migration use Ecto.Migration
def up do def up do
IO.puts("\n
#########################################################
# If the CREATE EXTENSION or DROP EXTENSION calls fail, #
# please manually execute them with an authorized #
# PostgreSQL user with SUPER USER role. #
#########################################################
\n
")
execute(""" execute("""
CREATE TYPE datetimetz AS ( CREATE TYPE datetimetz AS (
dt timestamptz, dt timestamptz,
@ -10,10 +19,23 @@ defmodule Mobilizon.Repo.Migrations.Prerequites do
""") """)
execute("CREATE EXTENSION IF NOT EXISTS postgis") execute("CREATE EXTENSION IF NOT EXISTS postgis")
execute("CREATE EXTENSION IF NOT EXISTS pg_trgm")
execute("CREATE EXTENSION IF NOT EXISTS unaccent")
end end
def down do def down do
IO.puts("\n
#########################################################
# If the CREATE EXTENSION or DROP EXTENSION calls fail, #
# please manually execute them with an authorized #
# PostgreSQL user with SUPER USER role. #
#########################################################
\n
")
execute("DROP TYPE IF EXISTS datetimetz;") execute("DROP TYPE IF EXISTS datetimetz;")
execute("DROP EXTENSION IF EXISTS postgis") execute("DROP EXTENSION IF EXISTS postgis")
execute("DROP EXTENSION IF EXISTS pg_trgm")
execute("DROP EXTENSION IF EXISTS unaccent")
end end
end end

View file

@ -0,0 +1,48 @@
defmodule Mobilizon.Repo.Migrations.CreateSearchIndexes do
use Ecto.Migration
def change do
IO.puts("\n
#########################################################
# If the CREATE EXTENSION or DROP EXTENSION calls fail, #
# please manually execute them with an authorized #
# PostgreSQL user with SUPER USER role. #
#########################################################
\n
")
try do
execute("CREATE EXTENSION IF NOT EXISTS pg_trgm", "DROP EXTENSION IF EXISTS pg_trgm")
execute("CREATE EXTENSION IF NOT EXISTS unaccent", "DROP EXTENSION IF EXISTS unaccent")
execute(
"CREATE OR REPLACE FUNCTION public.f_unaccent(text)
RETURNS text AS
$func$
SELECT public.unaccent('public.unaccent', $1)
$func$ LANGUAGE sql IMMUTABLE;",
"DROP FUNCTION IF EXISTS public.f_unaccent"
)
execute(
"CREATE INDEX \"event_title_trigram\" ON \"events\" USING GIN (f_unaccent(title) gin_trgm_ops)",
"DROP INDEX IF EXISTS event_title_trigram"
)
execute(
"CREATE INDEX \"actor_preferred_username_trigram\" ON \"actors\"
USING GIN (f_unaccent(preferred_username) gin_trgm_ops)",
"DROP INDEX IF EXISTS actor_preferred_username_trigram"
)
execute(
"CREATE INDEX \"actor_name_trigram\" ON \"actors\"
USING GIN (f_unaccent(name) gin_trgm_ops)",
"DROP INDEX IF EXISTS actor_name_trigram"
)
rescue
e in Postgrex.Error ->
IO.puts(e.message)
end
end
end

View file

@ -44,7 +44,7 @@ defmodule Mobilizon.ActorsTest do
setup do setup do
user = insert(:user) user = insert(:user)
actor = insert(:actor, user: user) actor = insert(:actor, user: user, preferred_username: "tcit")
{:ok, actor: actor} {:ok, actor: actor}
end end
@ -177,11 +177,10 @@ defmodule Mobilizon.ActorsTest do
test "test find_local_by_username/1 returns local actors with similar usernames", %{ test "test find_local_by_username/1 returns local actors with similar usernames", %{
actor: actor actor: actor
} do } do
actor2 = insert(:actor) actor2 = insert(:actor, preferred_username: "tcit")
[%Actor{id: actor_found_id} | tail] = Actors.find_local_by_username("thomas") [%Actor{id: actor_found_id} | tail] = Actors.find_local_by_username("tcit")
%Actor{id: actor2_found_id} = hd(tail) %Actor{id: actor2_found_id} = hd(tail)
assert actor_found_id == actor.id assert MapSet.new([actor_found_id, actor2_found_id]) == MapSet.new([actor.id, actor2.id])
assert actor2_found_id == actor2.id
end end
test "test find_actors_by_username_or_name/1 returns actors with similar usernames", %{ test "test find_actors_by_username_or_name/1 returns actors with similar usernames", %{
@ -189,7 +188,7 @@ defmodule Mobilizon.ActorsTest do
} do } do
use_cassette "actors/remote_actor_mastodon_tcit" do use_cassette "actors/remote_actor_mastodon_tcit" do
with {:ok, %Actor{id: actor2_id}} <- Actors.get_or_fetch_by_url(@remote_account_url) do with {:ok, %Actor{id: actor2_id}} <- Actors.get_or_fetch_by_url(@remote_account_url) do
actors_ids = Actors.find_actors_by_username_or_name("t") |> Enum.map(& &1.id) actors_ids = Actors.find_actors_by_username_or_name("tcit") |> Enum.map(& &1.id)
assert MapSet.new(actors_ids) == MapSet.new([actor2_id, actor_id]) assert MapSet.new(actors_ids) == MapSet.new([actor2_id, actor_id])
end end
end end
@ -200,26 +199,6 @@ defmodule Mobilizon.ActorsTest do
assert actors == [] assert actors == []
end end
test "test search/1 returns accounts for search with existing accounts", %{actor: actor} do
with {:ok, [%Actor{id: actor_found_id}]} <- Actors.search("t") do
assert actor_found_id == actor.id
end
end
test "test search/1 returns accounts for search with non existent accounts" do
assert {:ok, []} == Actors.search("nonexistent")
end
test "test search/1 returns accounts for search with existing remote accounts" do
with {:ok, [%Actor{preferred_username: username}]} <- Actors.search("tcit@framapiaf.org") do
assert username == "tcit"
end
end
test "test search/1 returns accounts for search with non existent remote accounts" do
assert {:error, "No ActivityPub URL found in WebFinger"} == Actors.search("tcit@yolo.tld")
end
test "test get_public_key_for_url/1 with local actor", %{actor: actor} do test "test get_public_key_for_url/1 with local actor", %{actor: actor} do
assert Actor.get_public_key_for_url(actor.url) == assert Actor.get_public_key_for_url(actor.url) ==
actor.keys |> Mobilizon.Actors.Actor.prepare_public_key() actor.keys |> Mobilizon.Actors.Actor.prepare_public_key()

View file

@ -59,12 +59,12 @@ defmodule Mobilizon.EventsTest do
assert title == hd(Events.find_events_by_name(event.title)).title assert title == hd(Events.find_events_by_name(event.title)).title
%Event{title: title2} = event2 = insert(:event, title: "Special event") %Event{title: title2} = event2 = insert(:event, title: "Special event")
assert event2.title == hd(Events.find_events_by_name("Special")).title assert event2.title == Events.find_events_by_name("Special") |> hd() |> Map.get(:title)
assert event2.title == hd(Events.find_events_by_name(" Special ")).title assert event2.title == Events.find_events_by_name(" Special ") |> hd() |> Map.get(:title)
assert title == hd(Events.find_events_by_name("")).title assert title == Events.find_events_by_name("") |> hd() |> Map.get(:title)
assert title2 == hd(tl(Events.find_events_by_name(""))).title assert title2 == Events.find_events_by_name("") |> tl |> hd() |> Map.get(:title)
end end
test "find_close_events/3 returns events in the area" do test "find_close_events/3 returns events in the area" do

View file

@ -0,0 +1,55 @@
defmodule MobilizonWeb.API.SearchTest do
use ExUnit.Case, async: false
alias Mobilizon.Events
alias Mobilizon.Events.Event
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Service.ActivityPub
alias MobilizonWeb.API.Search
import Mock
test "search an user by username" do
with_mock ActivityPub,
find_or_make_actor_from_nickname: fn "toto@domain.tld" -> {:ok, %Actor{id: 1}} end do
assert {:ok, %Actor{id: 1}} == Search.search("toto@domain.tld")
assert_called(ActivityPub.find_or_make_actor_from_nickname("toto@domain.tld"))
end
end
test "search something by URL" do
with_mock ActivityPub,
fetch_object_from_url: fn "https://social.tcit.fr/users/tcit" -> {:ok, %Actor{id: 1}} end do
assert {:ok, %Actor{id: 1}} == Search.search("https://social.tcit.fr/users/tcit")
assert_called(ActivityPub.fetch_object_from_url("https://social.tcit.fr/users/tcit"))
end
end
test "search everything" do
with_mocks([
{Actors, [], [find_actors_by_username_or_name: fn "toto", 1, 10 -> [%Actor{}] end]},
{Events, [], [find_events_by_name: fn "toto", 1, 10 -> [%Event{}] end]}
]) do
assert {:ok, [%Event{}, %Actor{}]} = Search.search("toto")
assert_called(Actors.find_actors_by_username_or_name("toto", 1, 10))
assert_called(Events.find_events_by_name("toto", 1, 10))
end
end
test "search actors" do
with_mock Actors,
find_actors_by_username_or_name: fn "toto", 1, 10 -> [%Actor{}] end do
assert {:ok, [%Actor{}]} = Search.search_actors("toto")
assert_called(Actors.find_actors_by_username_or_name("toto", 1, 10))
end
end
test "search events" do
with_mock Events,
find_events_by_name: fn "toto", 1, 10 -> [%Event{}] end do
assert {:ok, [%Event{}]} = Search.search_events("toto")
assert_called(Events.find_events_by_name("toto", 1, 10))
end
end
end

View file

@ -88,35 +88,6 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do
assert json_response(res, 200)["data"]["createEvent"]["title"] == "come to my event" assert json_response(res, 200)["data"]["createEvent"]["title"] == "come to my event"
end end
test "search_events_and_actors/3 finds events and actors", %{conn: conn, actor: actor} do
event = insert(:event, title: "test")
query = """
{
search(search: "test") {
...on Event {
title,
uuid,
__typename
},
...on Actor {
preferredUsername,
__typename
}
}
}
"""
res =
conn
|> get("/api", AbsintheHelpers.query_skeleton(query, "search"))
assert hd(json_response(res, 200)["data"]["search"])["uuid"] == to_string(event.uuid)
assert hd(tl(json_response(res, 200)["data"]["search"]))["preferredUsername"] ==
actor.preferred_username
end
test "list_events/3 returns events", context do test "list_events/3 returns events", context do
event = insert(:event) event = insert(:event)

View file

@ -0,0 +1,109 @@
defmodule MobilizonWeb.Resolvers.SearchResolverTest do
use MobilizonWeb.ConnCase
alias MobilizonWeb.AbsintheHelpers
import Mobilizon.Factory
setup %{conn: conn} do
user = insert(:user)
{:ok, conn: conn, user: user}
end
test "search_events_and_actors/3 finds events and actors with basic search", %{
conn: conn,
user: user
} do
actor = insert(:actor, user: user, preferred_username: "test")
event = insert(:event, title: "test")
query = """
{
search(search: "test") {
...on Event {
title,
uuid,
__typename
},
...on Actor {
preferredUsername,
__typename
}
}
}
"""
res =
conn
|> get("/api", AbsintheHelpers.query_skeleton(query, "search"))
assert hd(json_response(res, 200)["data"]["search"])["uuid"] == to_string(event.uuid)
assert hd(tl(json_response(res, 200)["data"]["search"]))["preferredUsername"] ==
actor.preferred_username
end
test "search_events_and_actors/3 finds events and actors with word search", %{
conn: conn,
user: user
} do
actor = insert(:actor, user: user, preferred_username: "toto", name: "I like pineapples")
event = insert(:event, title: "Pineapple fashion week")
# Elaborate query
query = """
{
search(search: "pineapple") {
...on Event {
title,
uuid,
__typename
},
...on Actor {
preferredUsername,
__typename
}
}
}
"""
res =
conn
|> get("/api", AbsintheHelpers.query_skeleton(query, "search"))
assert hd(json_response(res, 200)["data"]["search"])["uuid"] == to_string(event.uuid)
assert hd(tl(json_response(res, 200)["data"]["search"]))["preferredUsername"] ==
actor.preferred_username
end
test "search_events_and_actors/3 finds events and actors with accented search", %{
conn: conn,
user: user
} do
insert(:actor, user: user, preferred_username: "toto", name: "Torréfaction")
event = insert(:event, title: "Tour du monde des cafés")
# Elaborate query
query = """
{
search(search: "café") {
...on Event {
title,
uuid,
__typename
},
...on Actor {
preferredUsername,
__typename
}
}
}
"""
res =
conn
|> get("/api", AbsintheHelpers.query_skeleton(query, "search"))
assert hd(json_response(res, 200)["data"]["search"])["uuid"] == to_string(event.uuid)
end
end