logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma

timeline_controller_test.exs (19498B)


  1. # Pleroma: A lightweight social networking server
  2. # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
  3. # SPDX-License-Identifier: AGPL-3.0-only
  4. defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
  5. use Pleroma.Web.ConnCase
  6. import Pleroma.Factory
  7. import Tesla.Mock
  8. alias Pleroma.Config
  9. alias Pleroma.User
  10. alias Pleroma.Web.CommonAPI
  11. setup do
  12. mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
  13. :ok
  14. end
  15. describe "home" do
  16. setup do: oauth_access(["read:statuses"])
  17. test "does NOT embed account/pleroma/relationship in statuses", %{
  18. user: user,
  19. conn: conn
  20. } do
  21. other_user = insert(:user)
  22. {:ok, _} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
  23. response =
  24. conn
  25. |> assign(:user, user)
  26. |> get("/api/v1/timelines/home")
  27. |> json_response_and_validate_schema(200)
  28. assert Enum.all?(response, fn n ->
  29. get_in(n, ["account", "pleroma", "relationship"]) == %{}
  30. end)
  31. end
  32. test "the home timeline when the direct messages are excluded", %{user: user, conn: conn} do
  33. {:ok, public_activity} = CommonAPI.post(user, %{status: ".", visibility: "public"})
  34. {:ok, direct_activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"})
  35. {:ok, unlisted_activity} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"})
  36. {:ok, private_activity} = CommonAPI.post(user, %{status: ".", visibility: "private"})
  37. conn = get(conn, "/api/v1/timelines/home?exclude_visibilities[]=direct")
  38. assert status_ids = json_response_and_validate_schema(conn, :ok) |> Enum.map(& &1["id"])
  39. assert public_activity.id in status_ids
  40. assert unlisted_activity.id in status_ids
  41. assert private_activity.id in status_ids
  42. refute direct_activity.id in status_ids
  43. end
  44. end
  45. describe "public" do
  46. @tag capture_log: true
  47. test "the public timeline", %{conn: conn} do
  48. user = insert(:user)
  49. {:ok, activity} = CommonAPI.post(user, %{status: "test"})
  50. _activity = insert(:note_activity, local: false)
  51. conn = get(conn, "/api/v1/timelines/public?local=False")
  52. assert length(json_response_and_validate_schema(conn, :ok)) == 2
  53. conn = get(build_conn(), "/api/v1/timelines/public?local=True")
  54. assert [%{"content" => "test"}] = json_response_and_validate_schema(conn, :ok)
  55. conn = get(build_conn(), "/api/v1/timelines/public?local=1")
  56. assert [%{"content" => "test"}] = json_response_and_validate_schema(conn, :ok)
  57. # does not contain repeats
  58. {:ok, _} = CommonAPI.repeat(activity.id, user)
  59. conn = get(build_conn(), "/api/v1/timelines/public?local=true")
  60. assert [_] = json_response_and_validate_schema(conn, :ok)
  61. end
  62. test "the public timeline includes only public statuses for an authenticated user" do
  63. %{user: user, conn: conn} = oauth_access(["read:statuses"])
  64. {:ok, _activity} = CommonAPI.post(user, %{status: "test"})
  65. {:ok, _activity} = CommonAPI.post(user, %{status: "test", visibility: "private"})
  66. {:ok, _activity} = CommonAPI.post(user, %{status: "test", visibility: "unlisted"})
  67. {:ok, _activity} = CommonAPI.post(user, %{status: "test", visibility: "direct"})
  68. res_conn = get(conn, "/api/v1/timelines/public")
  69. assert length(json_response_and_validate_schema(res_conn, 200)) == 1
  70. end
  71. test "doesn't return replies if follower is posting with blocked user" do
  72. %{conn: conn, user: blocker} = oauth_access(["read:statuses"])
  73. [blockee, friend] = insert_list(2, :user)
  74. {:ok, blocker} = User.follow(blocker, friend)
  75. {:ok, _} = User.block(blocker, blockee)
  76. conn = assign(conn, :user, blocker)
  77. {:ok, %{id: activity_id} = activity} = CommonAPI.post(friend, %{status: "hey!"})
  78. {:ok, reply_from_blockee} =
  79. CommonAPI.post(blockee, %{status: "heya", in_reply_to_status_id: activity})
  80. {:ok, _reply_from_friend} =
  81. CommonAPI.post(friend, %{status: "status", in_reply_to_status_id: reply_from_blockee})
  82. # Still shows replies from yourself
  83. {:ok, %{id: reply_from_me}} =
  84. CommonAPI.post(blocker, %{status: "status", in_reply_to_status_id: reply_from_blockee})
  85. response =
  86. get(conn, "/api/v1/timelines/public")
  87. |> json_response_and_validate_schema(200)
  88. assert length(response) == 2
  89. [%{"id" => ^reply_from_me}, %{"id" => ^activity_id}] = response
  90. end
  91. test "doesn't return replies if follow is posting with users from blocked domain" do
  92. %{conn: conn, user: blocker} = oauth_access(["read:statuses"])
  93. friend = insert(:user)
  94. blockee = insert(:user, ap_id: "https://example.com/users/blocked")
  95. {:ok, blocker} = User.follow(blocker, friend)
  96. {:ok, blocker} = User.block_domain(blocker, "example.com")
  97. conn = assign(conn, :user, blocker)
  98. {:ok, %{id: activity_id} = activity} = CommonAPI.post(friend, %{status: "hey!"})
  99. {:ok, reply_from_blockee} =
  100. CommonAPI.post(blockee, %{status: "heya", in_reply_to_status_id: activity})
  101. {:ok, _reply_from_friend} =
  102. CommonAPI.post(friend, %{status: "status", in_reply_to_status_id: reply_from_blockee})
  103. res_conn = get(conn, "/api/v1/timelines/public")
  104. activities = json_response_and_validate_schema(res_conn, 200)
  105. [%{"id" => ^activity_id}] = activities
  106. end
  107. end
  108. defp local_and_remote_activities do
  109. insert(:note_activity)
  110. insert(:note_activity, local: false)
  111. :ok
  112. end
  113. describe "public with restrict unauthenticated timeline for local and federated timelines" do
  114. setup do: local_and_remote_activities()
  115. setup do: clear_config([:restrict_unauthenticated, :timelines, :local], true)
  116. setup do: clear_config([:restrict_unauthenticated, :timelines, :federated], true)
  117. test "if user is unauthenticated", %{conn: conn} do
  118. res_conn = get(conn, "/api/v1/timelines/public?local=true")
  119. assert json_response_and_validate_schema(res_conn, :unauthorized) == %{
  120. "error" => "authorization required for timeline view"
  121. }
  122. res_conn = get(conn, "/api/v1/timelines/public?local=false")
  123. assert json_response_and_validate_schema(res_conn, :unauthorized) == %{
  124. "error" => "authorization required for timeline view"
  125. }
  126. end
  127. test "if user is authenticated" do
  128. %{conn: conn} = oauth_access(["read:statuses"])
  129. res_conn = get(conn, "/api/v1/timelines/public?local=true")
  130. assert length(json_response_and_validate_schema(res_conn, 200)) == 1
  131. res_conn = get(conn, "/api/v1/timelines/public?local=false")
  132. assert length(json_response_and_validate_schema(res_conn, 200)) == 2
  133. end
  134. end
  135. describe "public with restrict unauthenticated timeline for local" do
  136. setup do: local_and_remote_activities()
  137. setup do: clear_config([:restrict_unauthenticated, :timelines, :local], true)
  138. test "if user is unauthenticated", %{conn: conn} do
  139. res_conn = get(conn, "/api/v1/timelines/public?local=true")
  140. assert json_response_and_validate_schema(res_conn, :unauthorized) == %{
  141. "error" => "authorization required for timeline view"
  142. }
  143. res_conn = get(conn, "/api/v1/timelines/public?local=false")
  144. assert length(json_response_and_validate_schema(res_conn, 200)) == 2
  145. end
  146. test "if user is authenticated", %{conn: _conn} do
  147. %{conn: conn} = oauth_access(["read:statuses"])
  148. res_conn = get(conn, "/api/v1/timelines/public?local=true")
  149. assert length(json_response_and_validate_schema(res_conn, 200)) == 1
  150. res_conn = get(conn, "/api/v1/timelines/public?local=false")
  151. assert length(json_response_and_validate_schema(res_conn, 200)) == 2
  152. end
  153. end
  154. describe "public with restrict unauthenticated timeline for remote" do
  155. setup do: local_and_remote_activities()
  156. setup do: clear_config([:restrict_unauthenticated, :timelines, :federated], true)
  157. test "if user is unauthenticated", %{conn: conn} do
  158. res_conn = get(conn, "/api/v1/timelines/public?local=true")
  159. assert length(json_response_and_validate_schema(res_conn, 200)) == 1
  160. res_conn = get(conn, "/api/v1/timelines/public?local=false")
  161. assert json_response_and_validate_schema(res_conn, :unauthorized) == %{
  162. "error" => "authorization required for timeline view"
  163. }
  164. end
  165. test "if user is authenticated", %{conn: _conn} do
  166. %{conn: conn} = oauth_access(["read:statuses"])
  167. res_conn = get(conn, "/api/v1/timelines/public?local=true")
  168. assert length(json_response_and_validate_schema(res_conn, 200)) == 1
  169. res_conn = get(conn, "/api/v1/timelines/public?local=false")
  170. assert length(json_response_and_validate_schema(res_conn, 200)) == 2
  171. end
  172. end
  173. describe "direct" do
  174. test "direct timeline", %{conn: conn} do
  175. user_one = insert(:user)
  176. user_two = insert(:user)
  177. {:ok, user_two} = User.follow(user_two, user_one)
  178. {:ok, direct} =
  179. CommonAPI.post(user_one, %{
  180. status: "Hi @#{user_two.nickname}!",
  181. visibility: "direct"
  182. })
  183. {:ok, _follower_only} =
  184. CommonAPI.post(user_one, %{
  185. status: "Hi @#{user_two.nickname}!",
  186. visibility: "private"
  187. })
  188. conn_user_two =
  189. conn
  190. |> assign(:user, user_two)
  191. |> assign(:token, insert(:oauth_token, user: user_two, scopes: ["read:statuses"]))
  192. # Only direct should be visible here
  193. res_conn = get(conn_user_two, "api/v1/timelines/direct")
  194. assert [status] = json_response_and_validate_schema(res_conn, :ok)
  195. assert %{"visibility" => "direct"} = status
  196. assert status["url"] != direct.data["id"]
  197. # User should be able to see their own direct message
  198. res_conn =
  199. build_conn()
  200. |> assign(:user, user_one)
  201. |> assign(:token, insert(:oauth_token, user: user_one, scopes: ["read:statuses"]))
  202. |> get("api/v1/timelines/direct")
  203. [status] = json_response_and_validate_schema(res_conn, :ok)
  204. assert %{"visibility" => "direct"} = status
  205. # Both should be visible here
  206. res_conn = get(conn_user_two, "api/v1/timelines/home")
  207. [_s1, _s2] = json_response_and_validate_schema(res_conn, :ok)
  208. # Test pagination
  209. Enum.each(1..20, fn _ ->
  210. {:ok, _} =
  211. CommonAPI.post(user_one, %{
  212. status: "Hi @#{user_two.nickname}!",
  213. visibility: "direct"
  214. })
  215. end)
  216. res_conn = get(conn_user_two, "api/v1/timelines/direct")
  217. statuses = json_response_and_validate_schema(res_conn, :ok)
  218. assert length(statuses) == 20
  219. max_id = List.last(statuses)["id"]
  220. res_conn = get(conn_user_two, "api/v1/timelines/direct?max_id=#{max_id}")
  221. assert [status] = json_response_and_validate_schema(res_conn, :ok)
  222. assert status["url"] != direct.data["id"]
  223. end
  224. test "doesn't include DMs from blocked users" do
  225. %{user: blocker, conn: conn} = oauth_access(["read:statuses"])
  226. blocked = insert(:user)
  227. other_user = insert(:user)
  228. {:ok, _user_relationship} = User.block(blocker, blocked)
  229. {:ok, _blocked_direct} =
  230. CommonAPI.post(blocked, %{
  231. status: "Hi @#{blocker.nickname}!",
  232. visibility: "direct"
  233. })
  234. {:ok, direct} =
  235. CommonAPI.post(other_user, %{
  236. status: "Hi @#{blocker.nickname}!",
  237. visibility: "direct"
  238. })
  239. res_conn = get(conn, "api/v1/timelines/direct")
  240. [status] = json_response_and_validate_schema(res_conn, :ok)
  241. assert status["id"] == direct.id
  242. end
  243. end
  244. describe "list" do
  245. setup do: oauth_access(["read:lists"])
  246. test "does not contain retoots", %{user: user, conn: conn} do
  247. other_user = insert(:user)
  248. {:ok, activity_one} = CommonAPI.post(user, %{status: "Marisa is cute."})
  249. {:ok, activity_two} = CommonAPI.post(other_user, %{status: "Marisa is stupid."})
  250. {:ok, _} = CommonAPI.repeat(activity_one.id, other_user)
  251. {:ok, list} = Pleroma.List.create("name", user)
  252. {:ok, list} = Pleroma.List.follow(list, other_user)
  253. conn = get(conn, "/api/v1/timelines/list/#{list.id}")
  254. assert [%{"id" => id}] = json_response_and_validate_schema(conn, :ok)
  255. assert id == to_string(activity_two.id)
  256. end
  257. test "works with pagination", %{user: user, conn: conn} do
  258. other_user = insert(:user)
  259. {:ok, list} = Pleroma.List.create("name", user)
  260. {:ok, list} = Pleroma.List.follow(list, other_user)
  261. Enum.each(1..30, fn i ->
  262. CommonAPI.post(other_user, %{status: "post number #{i}"})
  263. end)
  264. res =
  265. get(conn, "/api/v1/timelines/list/#{list.id}?limit=1")
  266. |> json_response_and_validate_schema(:ok)
  267. assert length(res) == 1
  268. [first] = res
  269. res =
  270. get(conn, "/api/v1/timelines/list/#{list.id}?max_id=#{first["id"]}&limit=30")
  271. |> json_response_and_validate_schema(:ok)
  272. assert length(res) == 29
  273. end
  274. test "list timeline", %{user: user, conn: conn} do
  275. other_user = insert(:user)
  276. {:ok, _activity_one} = CommonAPI.post(user, %{status: "Marisa is cute."})
  277. {:ok, activity_two} = CommonAPI.post(other_user, %{status: "Marisa is cute."})
  278. {:ok, list} = Pleroma.List.create("name", user)
  279. {:ok, list} = Pleroma.List.follow(list, other_user)
  280. conn = get(conn, "/api/v1/timelines/list/#{list.id}")
  281. assert [%{"id" => id}] = json_response_and_validate_schema(conn, :ok)
  282. assert id == to_string(activity_two.id)
  283. end
  284. test "list timeline does not leak non-public statuses for unfollowed users", %{
  285. user: user,
  286. conn: conn
  287. } do
  288. other_user = insert(:user)
  289. {:ok, activity_one} = CommonAPI.post(other_user, %{status: "Marisa is cute."})
  290. {:ok, _activity_two} =
  291. CommonAPI.post(other_user, %{
  292. status: "Marisa is cute.",
  293. visibility: "private"
  294. })
  295. {:ok, list} = Pleroma.List.create("name", user)
  296. {:ok, list} = Pleroma.List.follow(list, other_user)
  297. conn = get(conn, "/api/v1/timelines/list/#{list.id}")
  298. assert [%{"id" => id}] = json_response_and_validate_schema(conn, :ok)
  299. assert id == to_string(activity_one.id)
  300. end
  301. end
  302. describe "hashtag" do
  303. setup do: oauth_access(["n/a"])
  304. @tag capture_log: true
  305. test "hashtag timeline", %{conn: conn} do
  306. following = insert(:user)
  307. {:ok, activity} = CommonAPI.post(following, %{status: "test #2hu"})
  308. nconn = get(conn, "/api/v1/timelines/tag/2hu")
  309. assert [%{"id" => id}] = json_response_and_validate_schema(nconn, :ok)
  310. assert id == to_string(activity.id)
  311. # works for different capitalization too
  312. nconn = get(conn, "/api/v1/timelines/tag/2HU")
  313. assert [%{"id" => id}] = json_response_and_validate_schema(nconn, :ok)
  314. assert id == to_string(activity.id)
  315. end
  316. test "multi-hashtag timeline", %{conn: conn} do
  317. user = insert(:user)
  318. {:ok, activity_test} = CommonAPI.post(user, %{status: "#test"})
  319. {:ok, activity_test1} = CommonAPI.post(user, %{status: "#test #test1"})
  320. {:ok, activity_none} = CommonAPI.post(user, %{status: "#test #none"})
  321. any_test = get(conn, "/api/v1/timelines/tag/test?any[]=test1")
  322. [status_none, status_test1, status_test] = json_response_and_validate_schema(any_test, :ok)
  323. assert to_string(activity_test.id) == status_test["id"]
  324. assert to_string(activity_test1.id) == status_test1["id"]
  325. assert to_string(activity_none.id) == status_none["id"]
  326. restricted_test = get(conn, "/api/v1/timelines/tag/test?all[]=test1&none[]=none")
  327. assert [status_test1] == json_response_and_validate_schema(restricted_test, :ok)
  328. all_test = get(conn, "/api/v1/timelines/tag/test?all[]=none")
  329. assert [status_none] == json_response_and_validate_schema(all_test, :ok)
  330. end
  331. end
  332. describe "hashtag timeline handling of :restrict_unauthenticated setting" do
  333. setup do
  334. user = insert(:user)
  335. {:ok, activity1} = CommonAPI.post(user, %{status: "test #tag1"})
  336. {:ok, _activity2} = CommonAPI.post(user, %{status: "test #tag1"})
  337. activity1
  338. |> Ecto.Changeset.change(%{local: false})
  339. |> Pleroma.Repo.update()
  340. base_uri = "/api/v1/timelines/tag/tag1"
  341. error_response = %{"error" => "authorization required for timeline view"}
  342. %{base_uri: base_uri, error_response: error_response}
  343. end
  344. defp ensure_authenticated_access(base_uri) do
  345. %{conn: auth_conn} = oauth_access(["read:statuses"])
  346. res_conn = get(auth_conn, "#{base_uri}?local=true")
  347. assert length(json_response(res_conn, 200)) == 1
  348. res_conn = get(auth_conn, "#{base_uri}?local=false")
  349. assert length(json_response(res_conn, 200)) == 2
  350. end
  351. test "with default settings on private instances, returns 403 for unauthenticated users", %{
  352. conn: conn,
  353. base_uri: base_uri,
  354. error_response: error_response
  355. } do
  356. clear_config([:instance, :public], false)
  357. clear_config([:restrict_unauthenticated, :timelines])
  358. for local <- [true, false] do
  359. res_conn = get(conn, "#{base_uri}?local=#{local}")
  360. assert json_response(res_conn, :unauthorized) == error_response
  361. end
  362. ensure_authenticated_access(base_uri)
  363. end
  364. test "with `%{local: true, federated: true}`, returns 403 for unauthenticated users", %{
  365. conn: conn,
  366. base_uri: base_uri,
  367. error_response: error_response
  368. } do
  369. clear_config([:restrict_unauthenticated, :timelines, :local], true)
  370. clear_config([:restrict_unauthenticated, :timelines, :federated], true)
  371. for local <- [true, false] do
  372. res_conn = get(conn, "#{base_uri}?local=#{local}")
  373. assert json_response(res_conn, :unauthorized) == error_response
  374. end
  375. ensure_authenticated_access(base_uri)
  376. end
  377. test "with `%{local: false, federated: true}`, forbids unauthenticated access to federated timeline",
  378. %{conn: conn, base_uri: base_uri, error_response: error_response} do
  379. clear_config([:restrict_unauthenticated, :timelines, :local], false)
  380. clear_config([:restrict_unauthenticated, :timelines, :federated], true)
  381. res_conn = get(conn, "#{base_uri}?local=true")
  382. assert length(json_response(res_conn, 200)) == 1
  383. res_conn = get(conn, "#{base_uri}?local=false")
  384. assert json_response(res_conn, :unauthorized) == error_response
  385. ensure_authenticated_access(base_uri)
  386. end
  387. test "with `%{local: true, federated: false}`, forbids unauthenticated access to public timeline" <>
  388. "(but not to local public activities which are delivered as part of federated timeline)",
  389. %{conn: conn, base_uri: base_uri, error_response: error_response} do
  390. clear_config([:restrict_unauthenticated, :timelines, :local], true)
  391. clear_config([:restrict_unauthenticated, :timelines, :federated], false)
  392. res_conn = get(conn, "#{base_uri}?local=true")
  393. assert json_response(res_conn, :unauthorized) == error_response
  394. # Note: local activities get delivered as part of federated timeline
  395. res_conn = get(conn, "#{base_uri}?local=false")
  396. assert length(json_response(res_conn, 200)) == 2
  397. ensure_authenticated_access(base_uri)
  398. end
  399. end
  400. end