total rebase
[anni] / test / pleroma / web / mastodon_api / controllers / search_controller_test.exs
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
6   use Pleroma.Web.ConnCase
7
8   alias Pleroma.Object
9   alias Pleroma.Web.CommonAPI
10   alias Pleroma.Web.Endpoint
11   import Pleroma.Factory
12   import ExUnit.CaptureLog
13   import Tesla.Mock
14   import Mock
15
16   setup do
17     Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
18     :ok
19   end
20
21   setup_all do
22     mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
23     :ok
24   end
25
26   describe ".search2" do
27     test "it returns empty result if user or status search return undefined error", %{conn: conn} do
28       with_mocks [
29         {Pleroma.User, [], [search: fn _q, _o -> raise "Oops" end]},
30         {Pleroma.Activity, [], [search: fn _u, _q, _o -> raise "Oops" end]}
31       ] do
32         capture_log(fn ->
33           results =
34             conn
35             |> get("/api/v2/search?q=2hu")
36             |> json_response_and_validate_schema(200)
37
38           assert results["accounts"] == []
39           assert results["statuses"] == []
40         end) =~
41           "[error] Elixir.Pleroma.Web.MastodonAPI.SearchController search error: %RuntimeError{message: \"Oops\"}"
42       end
43     end
44
45     @tag :skip_darwin
46     test "search", %{conn: conn} do
47       user = insert(:user)
48       user_two = insert(:user, %{nickname: "shp@shitposter.club"})
49       user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"})
50
51       {:ok, activity} = CommonAPI.post(user, %{status: "This is about 2hu private 天子"})
52
53       {:ok, _activity} =
54         CommonAPI.post(user, %{
55           status: "This is about 2hu, but private",
56           visibility: "private"
57         })
58
59       {:ok, _} = CommonAPI.post(user_two, %{status: "This isn't"})
60
61       results =
62         conn
63         |> get("/api/v2/search?#{URI.encode_query(%{q: "2hu #private"})}")
64         |> json_response_and_validate_schema(200)
65
66       [account | _] = results["accounts"]
67       assert account["id"] == to_string(user_three.id)
68
69       assert results["hashtags"] == [
70                %{"name" => "private", "url" => "#{Endpoint.url()}/tag/private"}
71              ]
72
73       [status] = results["statuses"]
74       assert status["id"] == to_string(activity.id)
75
76       results =
77         get(conn, "/api/v2/search?q=天子")
78         |> json_response_and_validate_schema(200)
79
80       assert results["hashtags"] == [
81                %{"name" => "天子", "url" => "#{Endpoint.url()}/tag/天子"}
82              ]
83
84       [status] = results["statuses"]
85       assert status["id"] == to_string(activity.id)
86     end
87
88     test "search local-only status as an authenticated user" do
89       user = insert(:user)
90       %{conn: conn} = oauth_access(["read:search"])
91
92       {:ok, activity} =
93         CommonAPI.post(user, %{status: "This is about 2hu private 天子", visibility: "local"})
94
95       results =
96         conn
97         |> get("/api/v2/search?#{URI.encode_query(%{q: "2hu"})}")
98         |> json_response_and_validate_schema(200)
99
100       [status] = results["statuses"]
101       assert status["id"] == to_string(activity.id)
102     end
103
104     test "search local-only status as an unauthenticated user" do
105       user = insert(:user)
106       %{conn: conn} = oauth_access([])
107
108       {:ok, _activity} =
109         CommonAPI.post(user, %{status: "This is about 2hu private 天子", visibility: "local"})
110
111       results =
112         conn
113         |> get("/api/v2/search?#{URI.encode_query(%{q: "2hu"})}")
114         |> json_response_and_validate_schema(200)
115
116       assert [] = results["statuses"]
117     end
118
119     test "search local-only status as an anonymous user" do
120       user = insert(:user)
121
122       {:ok, _activity} =
123         CommonAPI.post(user, %{status: "This is about 2hu private 天子", visibility: "local"})
124
125       results =
126         build_conn()
127         |> get("/api/v2/search?#{URI.encode_query(%{q: "2hu"})}")
128         |> json_response_and_validate_schema(200)
129
130       assert [] = results["statuses"]
131     end
132
133     @tag capture_log: true
134     test "constructs hashtags from search query", %{conn: conn} do
135       results =
136         conn
137         |> get("/api/v2/search?#{URI.encode_query(%{q: "some text with #explicit #hashtags"})}")
138         |> json_response_and_validate_schema(200)
139
140       assert results["hashtags"] == [
141                %{"name" => "explicit", "url" => "#{Endpoint.url()}/tag/explicit"},
142                %{"name" => "hashtags", "url" => "#{Endpoint.url()}/tag/hashtags"}
143              ]
144
145       results =
146         conn
147         |> get("/api/v2/search?#{URI.encode_query(%{q: "john doe JOHN DOE"})}")
148         |> json_response_and_validate_schema(200)
149
150       assert results["hashtags"] == [
151                %{"name" => "john", "url" => "#{Endpoint.url()}/tag/john"},
152                %{"name" => "doe", "url" => "#{Endpoint.url()}/tag/doe"},
153                %{"name" => "JohnDoe", "url" => "#{Endpoint.url()}/tag/JohnDoe"}
154              ]
155
156       results =
157         conn
158         |> get("/api/v2/search?#{URI.encode_query(%{q: "accident-prone"})}")
159         |> json_response_and_validate_schema(200)
160
161       assert results["hashtags"] == [
162                %{"name" => "accident", "url" => "#{Endpoint.url()}/tag/accident"},
163                %{"name" => "prone", "url" => "#{Endpoint.url()}/tag/prone"},
164                %{"name" => "AccidentProne", "url" => "#{Endpoint.url()}/tag/AccidentProne"}
165              ]
166
167       results =
168         conn
169         |> get("/api/v2/search?#{URI.encode_query(%{q: "https://shpposter.club/users/shpuld"})}")
170         |> json_response_and_validate_schema(200)
171
172       assert results["hashtags"] == [
173                %{"name" => "shpuld", "url" => "#{Endpoint.url()}/tag/shpuld"}
174              ]
175
176       results =
177         conn
178         |> get(
179           "/api/v2/search?#{URI.encode_query(%{q: "https://www.washingtonpost.com/sports/2020/06/10/" <> "nascar-ban-display-confederate-flag-all-events-properties/"})}"
180         )
181         |> json_response_and_validate_schema(200)
182
183       assert results["hashtags"] == [
184                %{"name" => "nascar", "url" => "#{Endpoint.url()}/tag/nascar"},
185                %{"name" => "ban", "url" => "#{Endpoint.url()}/tag/ban"},
186                %{"name" => "display", "url" => "#{Endpoint.url()}/tag/display"},
187                %{"name" => "confederate", "url" => "#{Endpoint.url()}/tag/confederate"},
188                %{"name" => "flag", "url" => "#{Endpoint.url()}/tag/flag"},
189                %{"name" => "all", "url" => "#{Endpoint.url()}/tag/all"},
190                %{"name" => "events", "url" => "#{Endpoint.url()}/tag/events"},
191                %{"name" => "properties", "url" => "#{Endpoint.url()}/tag/properties"},
192                %{
193                  "name" => "NascarBanDisplayConfederateFlagAllEventsProperties",
194                  "url" =>
195                    "#{Endpoint.url()}/tag/NascarBanDisplayConfederateFlagAllEventsProperties"
196                }
197              ]
198     end
199
200     test "supports pagination of hashtags search results", %{conn: conn} do
201       results =
202         conn
203         |> get(
204           "/api/v2/search?#{URI.encode_query(%{q: "#some #text #with #hashtags", limit: 2, offset: 1})}"
205         )
206         |> json_response_and_validate_schema(200)
207
208       assert results["hashtags"] == [
209                %{"name" => "text", "url" => "#{Endpoint.url()}/tag/text"},
210                %{"name" => "with", "url" => "#{Endpoint.url()}/tag/with"}
211              ]
212     end
213
214     test "excludes a blocked users from search results", %{conn: conn} do
215       user = insert(:user)
216       user_smith = insert(:user, %{nickname: "Agent", name: "I love 2hu"})
217       user_neo = insert(:user, %{nickname: "Agent Neo", name: "Agent"})
218
219       {:ok, act1} = CommonAPI.post(user, %{status: "This is about 2hu private 天子"})
220       {:ok, act2} = CommonAPI.post(user_smith, %{status: "Agent Smith"})
221       {:ok, act3} = CommonAPI.post(user_neo, %{status: "Agent Smith"})
222       Pleroma.User.block(user, user_smith)
223
224       results =
225         conn
226         |> assign(:user, user)
227         |> assign(:token, insert(:oauth_token, user: user, scopes: ["read"]))
228         |> get("/api/v2/search?q=Agent")
229         |> json_response_and_validate_schema(200)
230
231       status_ids = Enum.map(results["statuses"], fn g -> g["id"] end)
232
233       assert act3.id in status_ids
234       refute act2.id in status_ids
235       refute act1.id in status_ids
236     end
237   end
238
239   describe ".account_search" do
240     test "account search", %{conn: conn} do
241       user_two = insert(:user, %{nickname: "shp@shitposter.club"})
242       user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"})
243
244       results =
245         conn
246         |> get("/api/v1/accounts/search?q=shp")
247         |> json_response_and_validate_schema(200)
248
249       result_ids = for result <- results, do: result["acct"]
250
251       assert user_two.nickname in result_ids
252       assert user_three.nickname in result_ids
253
254       results =
255         conn
256         |> get("/api/v1/accounts/search?q=2hu")
257         |> json_response_and_validate_schema(200)
258
259       result_ids = for result <- results, do: result["acct"]
260
261       assert user_three.nickname in result_ids
262     end
263
264     test "returns account if query contains a space", %{conn: conn} do
265       insert(:user, %{nickname: "shp@shitposter.club"})
266
267       results =
268         conn
269         |> get("/api/v1/accounts/search?q=shp@shitposter.club xxx")
270         |> json_response_and_validate_schema(200)
271
272       assert length(results) == 1
273     end
274   end
275
276   describe ".search" do
277     test "it returns empty result if user or status search return undefined error", %{conn: conn} do
278       with_mocks [
279         {Pleroma.User, [], [search: fn _q, _o -> raise "Oops" end]},
280         {Pleroma.Activity, [], [search: fn _u, _q, _o -> raise "Oops" end]}
281       ] do
282         capture_log(fn ->
283           results =
284             conn
285             |> get("/api/v1/search?q=2hu")
286             |> json_response_and_validate_schema(200)
287
288           assert results["accounts"] == []
289           assert results["statuses"] == []
290         end) =~
291           "[error] Elixir.Pleroma.Web.MastodonAPI.SearchController search error: %RuntimeError{message: \"Oops\"}"
292       end
293     end
294
295     test "search", %{conn: conn} do
296       user = insert(:user)
297       user_two = insert(:user, %{nickname: "shp@shitposter.club"})
298       user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"})
299
300       {:ok, activity} = CommonAPI.post(user, %{status: "This is about 2hu"})
301
302       {:ok, _activity} =
303         CommonAPI.post(user, %{
304           status: "This is about 2hu, but private",
305           visibility: "private"
306         })
307
308       {:ok, _} = CommonAPI.post(user_two, %{status: "This isn't"})
309
310       results =
311         conn
312         |> get("/api/v1/search?q=2hu")
313         |> json_response_and_validate_schema(200)
314
315       [account | _] = results["accounts"]
316       assert account["id"] == to_string(user_three.id)
317
318       assert results["hashtags"] == ["2hu"]
319
320       [status] = results["statuses"]
321       assert status["id"] == to_string(activity.id)
322     end
323
324     test "search fetches remote statuses and prefers them over other results", %{conn: conn} do
325       {:ok, %{id: activity_id}} =
326         CommonAPI.post(insert(:user), %{
327           status: "check out http://mastodon.example.org/@admin/99541947525187367"
328         })
329
330       %{"url" => result_url, "id" => result_id} =
331         conn
332         |> get("/api/v1/search?q=http://mastodon.example.org/@admin/99541947525187367")
333         |> json_response_and_validate_schema(200)
334         |> Map.get("statuses")
335         |> List.first()
336
337       refute match?(^result_id, activity_id)
338       assert match?(^result_url, "http://mastodon.example.org/@admin/99541947525187367")
339     end
340
341     test "search doesn't show statuses that it shouldn't", %{conn: conn} do
342       {:ok, activity} =
343         CommonAPI.post(insert(:user), %{
344           status: "This is about 2hu, but private",
345           visibility: "private"
346         })
347
348       capture_log(fn ->
349         q = Object.normalize(activity, fetch: false).data["id"]
350
351         results =
352           conn
353           |> get("/api/v1/search?q=#{q}")
354           |> json_response_and_validate_schema(200)
355
356         [] = results["statuses"]
357       end)
358     end
359
360     test "search fetches remote accounts", %{conn: conn} do
361       user = insert(:user)
362
363       query = URI.encode_query(%{q: "       mike@osada.macgirvin.com          ", resolve: true})
364
365       results =
366         conn
367         |> assign(:user, user)
368         |> assign(:token, insert(:oauth_token, user: user, scopes: ["read"]))
369         |> get("/api/v1/search?#{query}")
370         |> json_response_and_validate_schema(200)
371
372       [account] = results["accounts"]
373       assert account["acct"] == "mike@osada.macgirvin.com"
374     end
375
376     test "search doesn't fetch remote accounts if resolve is false", %{conn: conn} do
377       results =
378         conn
379         |> get("/api/v1/search?q=mike@osada.macgirvin.com&resolve=false")
380         |> json_response_and_validate_schema(200)
381
382       assert [] == results["accounts"]
383     end
384
385     test "search with limit and offset", %{conn: conn} do
386       user = insert(:user)
387       _user_two = insert(:user, %{nickname: "shp@shitposter.club"})
388       _user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"})
389
390       {:ok, _activity1} = CommonAPI.post(user, %{status: "This is about 2hu"})
391       {:ok, _activity2} = CommonAPI.post(user, %{status: "This is also about 2hu"})
392
393       result =
394         conn
395         |> get("/api/v1/search?q=2hu&limit=1")
396
397       assert results = json_response_and_validate_schema(result, 200)
398       assert [%{"id" => activity_id1}] = results["statuses"]
399       assert [_] = results["accounts"]
400
401       results =
402         conn
403         |> get("/api/v1/search?q=2hu&limit=1&offset=1")
404         |> json_response_and_validate_schema(200)
405
406       assert [%{"id" => activity_id2}] = results["statuses"]
407       assert [] = results["accounts"]
408
409       assert activity_id1 != activity_id2
410     end
411
412     test "search returns results only for the given type", %{conn: conn} do
413       user = insert(:user)
414       _user_two = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"})
415
416       {:ok, _activity} = CommonAPI.post(user, %{status: "This is about 2hu"})
417
418       assert %{"statuses" => [_activity], "accounts" => [], "hashtags" => []} =
419                conn
420                |> get("/api/v1/search?q=2hu&type=statuses")
421                |> json_response_and_validate_schema(200)
422
423       assert %{"statuses" => [], "accounts" => [_user_two], "hashtags" => []} =
424                conn
425                |> get("/api/v1/search?q=2hu&type=accounts")
426                |> json_response_and_validate_schema(200)
427     end
428
429     test "search uses account_id to filter statuses by the author", %{conn: conn} do
430       user = insert(:user, %{nickname: "shp@shitposter.club"})
431       user_two = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"})
432
433       {:ok, activity1} = CommonAPI.post(user, %{status: "This is about 2hu"})
434       {:ok, activity2} = CommonAPI.post(user_two, %{status: "This is also about 2hu"})
435
436       results =
437         conn
438         |> get("/api/v1/search?q=2hu&account_id=#{user.id}")
439         |> json_response_and_validate_schema(200)
440
441       assert [%{"id" => activity_id1}] = results["statuses"]
442       assert activity_id1 == activity1.id
443       assert [_] = results["accounts"]
444
445       results =
446         conn
447         |> get("/api/v1/search?q=2hu&account_id=#{user_two.id}")
448         |> json_response_and_validate_schema(200)
449
450       assert [%{"id" => activity_id2}] = results["statuses"]
451       assert activity_id2 == activity2.id
452     end
453   end
454 end