total rebase
[anni] / test / pleroma / web / twitter_api / remote_follow_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.TwitterAPI.RemoteFollowControllerTest do
6   use Pleroma.Web.ConnCase
7
8   alias Pleroma.MFA
9   alias Pleroma.MFA.TOTP
10   alias Pleroma.UnstubbedConfigMock, as: ConfigMock
11   alias Pleroma.User
12   alias Pleroma.Web.CommonAPI
13
14   import Ecto.Query
15   import ExUnit.CaptureLog
16   import Mox
17   import Pleroma.Factory
18
19   setup_all do: clear_config([:instance, :federating], true)
20   setup do: clear_config([:user, :deny_follow_blocked])
21
22   describe "GET /ostatus_subscribe - remote_follow/2" do
23     test "adds status to pleroma instance if the `acct` is a status", %{conn: conn} do
24       Tesla.Mock.mock(fn
25         %{method: :get, url: "https://mastodon.social/users/emelie/statuses/101849165031453009"} ->
26           %Tesla.Env{
27             status: 200,
28             headers: [{"content-type", "application/activity+json"}],
29             body: File.read!("test/fixtures/tesla_mock/status.emelie.json")
30           }
31
32         %{method: :get, url: "https://mastodon.social/users/emelie/collections/featured"} ->
33           %Tesla.Env{
34             status: 200,
35             headers: [{"content-type", "application/activity+json"}],
36             body:
37               File.read!("test/fixtures/users_mock/masto_featured.json")
38               |> String.replace("{{domain}}", "mastodon.social")
39               |> String.replace("{{nickname}}", "emelie")
40           }
41
42         %{method: :get, url: "https://mastodon.social/users/emelie"} ->
43           %Tesla.Env{
44             status: 200,
45             headers: [{"content-type", "application/activity+json"}],
46             body: File.read!("test/fixtures/tesla_mock/emelie.json")
47           }
48       end)
49
50       assert conn
51              |> get(
52                remote_follow_path(conn, :follow, %{
53                  acct: "https://mastodon.social/users/emelie/statuses/101849165031453009"
54                })
55              )
56              |> redirected_to() =~ "/notice/"
57     end
58
59     test "show follow account page if the `acct` is a account link", %{conn: conn} do
60       Tesla.Mock.mock(fn
61         %{method: :get, url: "https://mastodon.social/users/emelie"} ->
62           %Tesla.Env{
63             status: 200,
64             headers: [{"content-type", "application/activity+json"}],
65             body: File.read!("test/fixtures/tesla_mock/emelie.json")
66           }
67
68         %{method: :get, url: "https://mastodon.social/users/emelie/collections/featured"} ->
69           %Tesla.Env{
70             status: 200,
71             headers: [{"content-type", "application/activity+json"}],
72             body:
73               File.read!("test/fixtures/users_mock/masto_featured.json")
74               |> String.replace("{{domain}}", "mastodon.social")
75               |> String.replace("{{nickname}}", "emelie")
76           }
77       end)
78
79       response =
80         conn
81         |> get(remote_follow_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"}))
82         |> html_response(200)
83
84       assert response =~ "Log in to follow"
85     end
86
87     test "show follow page if the `acct` is a account link", %{conn: conn} do
88       Tesla.Mock.mock(fn
89         %{method: :get, url: "https://mastodon.social/users/emelie"} ->
90           %Tesla.Env{
91             status: 200,
92             headers: [{"content-type", "application/activity+json"}],
93             body: File.read!("test/fixtures/tesla_mock/emelie.json")
94           }
95
96         %{method: :get, url: "https://mastodon.social/users/emelie/collections/featured"} ->
97           %Tesla.Env{
98             status: 200,
99             headers: [{"content-type", "application/activity+json"}],
100             body:
101               File.read!("test/fixtures/users_mock/masto_featured.json")
102               |> String.replace("{{domain}}", "mastodon.social")
103               |> String.replace("{{nickname}}", "emelie")
104           }
105       end)
106
107       user = insert(:user)
108
109       response =
110         conn
111         |> assign(:user, user)
112         |> get(remote_follow_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"}))
113         |> html_response(200)
114
115       assert response =~ "Remote follow"
116     end
117
118     test "show follow page with error when user can not be fetched by `acct` link", %{conn: conn} do
119       Tesla.Mock.mock(fn
120         %{method: :get, url: "https://mastodon.social/users/not_found"} ->
121           %Tesla.Env{
122             status: 404
123           }
124       end)
125
126       user = insert(:user)
127
128       assert capture_log(fn ->
129                response =
130                  conn
131                  |> assign(:user, user)
132                  |> get(
133                    remote_follow_path(conn, :follow, %{
134                      acct: "https://mastodon.social/users/not_found"
135                    })
136                  )
137                  |> html_response(200)
138
139                assert response =~ "Error fetching user"
140              end) =~ ":not_found"
141     end
142   end
143
144   describe "POST /ostatus_subscribe - do_follow/2 with assigned user " do
145     test "required `follow | write:follows` scope", %{conn: conn} do
146       user = insert(:user)
147       user2 = insert(:user)
148       read_token = insert(:oauth_token, user: user, scopes: ["read"])
149
150       assert capture_log(fn ->
151                response =
152                  conn
153                  |> assign(:user, user)
154                  |> assign(:token, read_token)
155                  |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}})
156                  |> response(200)
157
158                assert response =~ "Error following account"
159              end) =~ "Insufficient permissions: follow | write:follows."
160     end
161
162     test "follows user", %{conn: conn} do
163       user = insert(:user)
164       user2 = insert(:user)
165
166       conn =
167         conn
168         |> assign(:user, user)
169         |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:follows"]))
170         |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}})
171
172       assert redirected_to(conn) == "/users/#{user2.id}"
173     end
174
175     test "returns error when user is deactivated", %{conn: conn} do
176       user = insert(:user, is_active: false)
177       user2 = insert(:user)
178
179       response =
180         conn
181         |> assign(:user, user)
182         |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}})
183         |> response(200)
184
185       assert response =~ "Error following account"
186     end
187
188     test "returns error when user is blocked", %{conn: conn} do
189       clear_config([:user, :deny_follow_blocked], true)
190       user = insert(:user)
191       user2 = insert(:user)
192
193       {:ok, _user_block} = Pleroma.User.block(user2, user)
194
195       response =
196         conn
197         |> assign(:user, user)
198         |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}})
199         |> response(200)
200
201       assert response =~ "Error following account"
202     end
203
204     test "returns error when followee not found", %{conn: conn} do
205       user = insert(:user)
206
207       response =
208         conn
209         |> assign(:user, user)
210         |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => "jimm"}})
211         |> response(200)
212
213       assert response =~ "Error following account"
214     end
215
216     test "returns success result when user already in followers", %{conn: conn} do
217       user = insert(:user)
218       user2 = insert(:user)
219       {:ok, _, _, _} = CommonAPI.follow(user, user2)
220
221       conn =
222         conn
223         |> assign(:user, refresh_record(user))
224         |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:follows"]))
225         |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}})
226
227       assert redirected_to(conn) == "/users/#{user2.id}"
228     end
229   end
230
231   describe "POST /ostatus_subscribe - follow/2 with enabled Two-Factor Auth " do
232     test "render the MFA login form", %{conn: conn} do
233       otp_secret = TOTP.generate_secret()
234
235       user =
236         insert(:user,
237           multi_factor_authentication_settings: %MFA.Settings{
238             enabled: true,
239             totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
240           }
241         )
242
243       user2 = insert(:user)
244
245       response =
246         conn
247         |> post(remote_follow_path(conn, :do_follow), %{
248           "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id}
249         })
250         |> response(200)
251
252       mfa_token = Pleroma.Repo.one(from(q in Pleroma.MFA.Token, where: q.user_id == ^user.id))
253
254       assert response =~ "Two-factor authentication"
255       assert response =~ "Authentication code"
256       assert response =~ mfa_token.token
257       refute user2.follower_address in User.following(user)
258     end
259
260     test "returns error when password is incorrect", %{conn: conn} do
261       otp_secret = TOTP.generate_secret()
262
263       user =
264         insert(:user,
265           multi_factor_authentication_settings: %MFA.Settings{
266             enabled: true,
267             totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
268           }
269         )
270
271       user2 = insert(:user)
272
273       response =
274         conn
275         |> post(remote_follow_path(conn, :do_follow), %{
276           "authorization" => %{"name" => user.nickname, "password" => "test1", "id" => user2.id}
277         })
278         |> response(200)
279
280       assert response =~ "Wrong username or password"
281       refute user2.follower_address in User.following(user)
282     end
283
284     test "follows", %{conn: conn} do
285       otp_secret = TOTP.generate_secret()
286
287       user =
288         insert(:user,
289           multi_factor_authentication_settings: %MFA.Settings{
290             enabled: true,
291             totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
292           }
293         )
294
295       {:ok, %{token: token}} = MFA.Token.create(user)
296
297       user2 = insert(:user)
298       otp_token = TOTP.generate_token(otp_secret)
299
300       conn =
301         conn
302         |> post(
303           remote_follow_path(conn, :do_follow),
304           %{
305             "mfa" => %{"code" => otp_token, "token" => token, "id" => user2.id}
306           }
307         )
308
309       assert redirected_to(conn) == "/users/#{user2.id}"
310       assert user2.follower_address in User.following(user)
311     end
312
313     test "returns error when auth code is incorrect", %{conn: conn} do
314       otp_secret = TOTP.generate_secret()
315
316       user =
317         insert(:user,
318           multi_factor_authentication_settings: %MFA.Settings{
319             enabled: true,
320             totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
321           }
322         )
323
324       {:ok, %{token: token}} = MFA.Token.create(user)
325
326       user2 = insert(:user)
327       otp_token = TOTP.generate_token(TOTP.generate_secret())
328
329       response =
330         conn
331         |> post(
332           remote_follow_path(conn, :do_follow),
333           %{
334             "mfa" => %{"code" => otp_token, "token" => token, "id" => user2.id}
335           }
336         )
337         |> response(200)
338
339       assert response =~ "Wrong authentication code"
340       refute user2.follower_address in User.following(user)
341     end
342   end
343
344   describe "POST /ostatus_subscribe - follow/2 without assigned user " do
345     test "follows", %{conn: conn} do
346       user = insert(:user)
347       user2 = insert(:user)
348
349       conn =
350         conn
351         |> post(remote_follow_path(conn, :do_follow), %{
352           "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id}
353         })
354
355       assert redirected_to(conn) == "/users/#{user2.id}"
356       assert user2.follower_address in User.following(user)
357     end
358
359     test "returns error when followee not found", %{conn: conn} do
360       user = insert(:user)
361
362       response =
363         conn
364         |> post(remote_follow_path(conn, :do_follow), %{
365           "authorization" => %{"name" => user.nickname, "password" => "test", "id" => "jimm"}
366         })
367         |> response(200)
368
369       assert response =~ "Error following account"
370     end
371
372     test "returns error when login invalid", %{conn: conn} do
373       user = insert(:user)
374
375       response =
376         conn
377         |> post(remote_follow_path(conn, :do_follow), %{
378           "authorization" => %{"name" => "jimm", "password" => "test", "id" => user.id}
379         })
380         |> response(200)
381
382       assert response =~ "Wrong username or password"
383     end
384
385     test "returns error when password invalid", %{conn: conn} do
386       user = insert(:user)
387       user2 = insert(:user)
388
389       response =
390         conn
391         |> post(remote_follow_path(conn, :do_follow), %{
392           "authorization" => %{"name" => user.nickname, "password" => "42", "id" => user2.id}
393         })
394         |> response(200)
395
396       assert response =~ "Wrong username or password"
397     end
398
399     test "returns error when user is blocked", %{conn: conn} do
400       clear_config([:user, :deny_follow_blocked], true)
401       user = insert(:user)
402       user2 = insert(:user)
403       {:ok, _user_block} = Pleroma.User.block(user2, user)
404
405       response =
406         conn
407         |> post(remote_follow_path(conn, :do_follow), %{
408           "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id}
409         })
410         |> response(200)
411
412       assert response =~ "Error following account"
413     end
414   end
415
416   describe "avatar url" do
417     test "without media proxy" do
418       clear_config([:media_proxy, :enabled], false)
419
420       user =
421         insert(:user, %{
422           local: false,
423           avatar: %{"url" => [%{"href" => "https://remote.org/avatar.png"}]}
424         })
425
426       avatar_url = Pleroma.Web.TwitterAPI.RemoteFollowView.avatar_url(user)
427
428       assert avatar_url == "https://remote.org/avatar.png"
429     end
430
431     test "with media proxy" do
432       clear_config([:media_proxy, :enabled], true)
433
434       ConfigMock
435       |> stub_with(Pleroma.Test.StaticConfig)
436
437       user =
438         insert(:user, %{
439           local: false,
440           avatar: %{"url" => [%{"href" => "https://remote.org/avatar.png"}]}
441         })
442
443       avatar_url = Pleroma.Web.TwitterAPI.RemoteFollowView.avatar_url(user)
444       url = Pleroma.Web.Endpoint.url()
445
446       assert String.starts_with?(avatar_url, url)
447     end
448
449     test "local avatar is not proxied" do
450       clear_config([:media_proxy, :enabled], true)
451
452       user =
453         insert(:user, %{
454           local: true,
455           avatar: %{"url" => [%{"href" => "#{Pleroma.Web.Endpoint.url()}/localuser/avatar.png"}]}
456         })
457
458       avatar_url = Pleroma.Web.TwitterAPI.RemoteFollowView.avatar_url(user)
459
460       assert avatar_url == "#{Pleroma.Web.Endpoint.url()}/localuser/avatar.png"
461     end
462   end
463
464   describe "GET /authorize_interaction - authorize_interaction/2" do
465     test "redirects to /ostatus_subscribe", %{conn: conn} do
466       Tesla.Mock.mock(fn
467         %{method: :get, url: "https://mastodon.social/users/emelie"} ->
468           %Tesla.Env{
469             status: 200,
470             headers: [{"content-type", "application/activity+json"}],
471             body: File.read!("test/fixtures/tesla_mock/emelie.json")
472           }
473
474         %{method: :get, url: "https://mastodon.social/users/emelie/collections/featured"} ->
475           %Tesla.Env{
476             status: 200,
477             headers: [{"content-type", "application/activity+json"}],
478             body:
479               File.read!("test/fixtures/users_mock/masto_featured.json")
480               |> String.replace("{{domain}}", "mastodon.social")
481               |> String.replace("{{nickname}}", "emelie")
482           }
483       end)
484
485       conn =
486         conn
487         |> get(
488           remote_follow_path(conn, :authorize_interaction, %{
489             uri: "https://mastodon.social/users/emelie"
490           })
491         )
492
493       assert redirected_to(conn) ==
494                remote_follow_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"})
495     end
496   end
497 end