Fork of Pleroma with site-specific changes and feature branches https://git.pleroma.social/pleroma/pleroma
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1301 lines
40KB

  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.StatusControllerTest do
  5. use Pleroma.Web.ConnCase
  6. alias Pleroma.Activity
  7. alias Pleroma.ActivityExpiration
  8. alias Pleroma.Config
  9. alias Pleroma.Conversation.Participation
  10. alias Pleroma.Object
  11. alias Pleroma.Repo
  12. alias Pleroma.ScheduledActivity
  13. alias Pleroma.Tests.ObanHelpers
  14. alias Pleroma.User
  15. alias Pleroma.Web.ActivityPub.ActivityPub
  16. alias Pleroma.Web.CommonAPI
  17. import Pleroma.Factory
  18. clear_config([:instance, :federating])
  19. clear_config([:instance, :allow_relay])
  20. clear_config([:rich_media, :enabled])
  21. describe "posting statuses" do
  22. setup do: oauth_access(["write:statuses"])
  23. test "posting a status does not increment reblog_count when relaying", %{conn: conn} do
  24. Pleroma.Config.put([:instance, :federating], true)
  25. Pleroma.Config.get([:instance, :allow_relay], true)
  26. response =
  27. conn
  28. |> post("api/v1/statuses", %{
  29. "content_type" => "text/plain",
  30. "source" => "Pleroma FE",
  31. "status" => "Hello world",
  32. "visibility" => "public"
  33. })
  34. |> json_response(200)
  35. assert response["reblogs_count"] == 0
  36. ObanHelpers.perform_all()
  37. response =
  38. conn
  39. |> get("api/v1/statuses/#{response["id"]}", %{})
  40. |> json_response(200)
  41. assert response["reblogs_count"] == 0
  42. end
  43. test "posting a status", %{conn: conn} do
  44. idempotency_key = "Pikachu rocks!"
  45. conn_one =
  46. conn
  47. |> put_req_header("idempotency-key", idempotency_key)
  48. |> post("/api/v1/statuses", %{
  49. "status" => "cofe",
  50. "spoiler_text" => "2hu",
  51. "sensitive" => "false"
  52. })
  53. {:ok, ttl} = Cachex.ttl(:idempotency_cache, idempotency_key)
  54. # Six hours
  55. assert ttl > :timer.seconds(6 * 60 * 60 - 1)
  56. assert %{"content" => "cofe", "id" => id, "spoiler_text" => "2hu", "sensitive" => false} =
  57. json_response(conn_one, 200)
  58. assert Activity.get_by_id(id)
  59. conn_two =
  60. conn
  61. |> put_req_header("idempotency-key", idempotency_key)
  62. |> post("/api/v1/statuses", %{
  63. "status" => "cofe",
  64. "spoiler_text" => "2hu",
  65. "sensitive" => "false"
  66. })
  67. assert %{"id" => second_id} = json_response(conn_two, 200)
  68. assert id == second_id
  69. conn_three =
  70. conn
  71. |> post("/api/v1/statuses", %{
  72. "status" => "cofe",
  73. "spoiler_text" => "2hu",
  74. "sensitive" => "false"
  75. })
  76. assert %{"id" => third_id} = json_response(conn_three, 200)
  77. refute id == third_id
  78. # An activity that will expire:
  79. # 2 hours
  80. expires_in = 120 * 60
  81. conn_four =
  82. conn
  83. |> post("api/v1/statuses", %{
  84. "status" => "oolong",
  85. "expires_in" => expires_in
  86. })
  87. assert fourth_response = %{"id" => fourth_id} = json_response(conn_four, 200)
  88. assert activity = Activity.get_by_id(fourth_id)
  89. assert expiration = ActivityExpiration.get_by_activity_id(fourth_id)
  90. estimated_expires_at =
  91. NaiveDateTime.utc_now()
  92. |> NaiveDateTime.add(expires_in)
  93. |> NaiveDateTime.truncate(:second)
  94. # This assert will fail if the test takes longer than a minute. I sure hope it never does:
  95. assert abs(NaiveDateTime.diff(expiration.scheduled_at, estimated_expires_at, :second)) < 60
  96. assert fourth_response["pleroma"]["expires_at"] ==
  97. NaiveDateTime.to_iso8601(expiration.scheduled_at)
  98. end
  99. test "it fails to create a status if `expires_in` is less or equal than an hour", %{
  100. conn: conn
  101. } do
  102. # 1 hour
  103. expires_in = 60 * 60
  104. assert %{"error" => "Expiry date is too soon"} =
  105. conn
  106. |> post("api/v1/statuses", %{
  107. "status" => "oolong",
  108. "expires_in" => expires_in
  109. })
  110. |> json_response(422)
  111. # 30 minutes
  112. expires_in = 30 * 60
  113. assert %{"error" => "Expiry date is too soon"} =
  114. conn
  115. |> post("api/v1/statuses", %{
  116. "status" => "oolong",
  117. "expires_in" => expires_in
  118. })
  119. |> json_response(422)
  120. end
  121. test "posting an undefined status with an attachment", %{user: user, conn: conn} do
  122. file = %Plug.Upload{
  123. content_type: "image/jpg",
  124. path: Path.absname("test/fixtures/image.jpg"),
  125. filename: "an_image.jpg"
  126. }
  127. {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
  128. conn =
  129. post(conn, "/api/v1/statuses", %{
  130. "media_ids" => [to_string(upload.id)]
  131. })
  132. assert json_response(conn, 200)
  133. end
  134. test "replying to a status", %{user: user, conn: conn} do
  135. {:ok, replied_to} = CommonAPI.post(user, %{"status" => "cofe"})
  136. conn =
  137. conn
  138. |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
  139. assert %{"content" => "xD", "id" => id} = json_response(conn, 200)
  140. activity = Activity.get_by_id(id)
  141. assert activity.data["context"] == replied_to.data["context"]
  142. assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
  143. end
  144. test "replying to a direct message with visibility other than direct", %{
  145. user: user,
  146. conn: conn
  147. } do
  148. {:ok, replied_to} = CommonAPI.post(user, %{"status" => "suya..", "visibility" => "direct"})
  149. Enum.each(["public", "private", "unlisted"], fn visibility ->
  150. conn =
  151. conn
  152. |> post("/api/v1/statuses", %{
  153. "status" => "@#{user.nickname} hey",
  154. "in_reply_to_id" => replied_to.id,
  155. "visibility" => visibility
  156. })
  157. assert json_response(conn, 422) == %{"error" => "The message visibility must be direct"}
  158. end)
  159. end
  160. test "posting a status with an invalid in_reply_to_id", %{conn: conn} do
  161. conn = post(conn, "/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => ""})
  162. assert %{"content" => "xD", "id" => id} = json_response(conn, 200)
  163. assert Activity.get_by_id(id)
  164. end
  165. test "posting a sensitive status", %{conn: conn} do
  166. conn = post(conn, "/api/v1/statuses", %{"status" => "cofe", "sensitive" => true})
  167. assert %{"content" => "cofe", "id" => id, "sensitive" => true} = json_response(conn, 200)
  168. assert Activity.get_by_id(id)
  169. end
  170. test "posting a fake status", %{conn: conn} do
  171. real_conn =
  172. post(conn, "/api/v1/statuses", %{
  173. "status" =>
  174. "\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it"
  175. })
  176. real_status = json_response(real_conn, 200)
  177. assert real_status
  178. assert Object.get_by_ap_id(real_status["uri"])
  179. real_status =
  180. real_status
  181. |> Map.put("id", nil)
  182. |> Map.put("url", nil)
  183. |> Map.put("uri", nil)
  184. |> Map.put("created_at", nil)
  185. |> Kernel.put_in(["pleroma", "conversation_id"], nil)
  186. fake_conn =
  187. post(conn, "/api/v1/statuses", %{
  188. "status" =>
  189. "\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it",
  190. "preview" => true
  191. })
  192. fake_status = json_response(fake_conn, 200)
  193. assert fake_status
  194. refute Object.get_by_ap_id(fake_status["uri"])
  195. fake_status =
  196. fake_status
  197. |> Map.put("id", nil)
  198. |> Map.put("url", nil)
  199. |> Map.put("uri", nil)
  200. |> Map.put("created_at", nil)
  201. |> Kernel.put_in(["pleroma", "conversation_id"], nil)
  202. assert real_status == fake_status
  203. end
  204. test "posting a status with OGP link preview", %{conn: conn} do
  205. Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
  206. Config.put([:rich_media, :enabled], true)
  207. conn =
  208. post(conn, "/api/v1/statuses", %{
  209. "status" => "https://example.com/ogp"
  210. })
  211. assert %{"id" => id, "card" => %{"title" => "The Rock"}} = json_response(conn, 200)
  212. assert Activity.get_by_id(id)
  213. end
  214. test "posting a direct status", %{conn: conn} do
  215. user2 = insert(:user)
  216. content = "direct cofe @#{user2.nickname}"
  217. conn = post(conn, "api/v1/statuses", %{"status" => content, "visibility" => "direct"})
  218. assert %{"id" => id} = response = json_response(conn, 200)
  219. assert response["visibility"] == "direct"
  220. assert response["pleroma"]["direct_conversation_id"]
  221. assert activity = Activity.get_by_id(id)
  222. assert activity.recipients == [user2.ap_id, conn.assigns[:user].ap_id]
  223. assert activity.data["to"] == [user2.ap_id]
  224. assert activity.data["cc"] == []
  225. end
  226. end
  227. describe "posting scheduled statuses" do
  228. setup do: oauth_access(["write:statuses"])
  229. test "creates a scheduled activity", %{conn: conn} do
  230. scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
  231. conn =
  232. post(conn, "/api/v1/statuses", %{
  233. "status" => "scheduled",
  234. "scheduled_at" => scheduled_at
  235. })
  236. assert %{"scheduled_at" => expected_scheduled_at} = json_response(conn, 200)
  237. assert expected_scheduled_at == CommonAPI.Utils.to_masto_date(scheduled_at)
  238. assert [] == Repo.all(Activity)
  239. end
  240. test "creates a scheduled activity with a media attachment", %{user: user, conn: conn} do
  241. scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
  242. file = %Plug.Upload{
  243. content_type: "image/jpg",
  244. path: Path.absname("test/fixtures/image.jpg"),
  245. filename: "an_image.jpg"
  246. }
  247. {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
  248. conn =
  249. post(conn, "/api/v1/statuses", %{
  250. "media_ids" => [to_string(upload.id)],
  251. "status" => "scheduled",
  252. "scheduled_at" => scheduled_at
  253. })
  254. assert %{"media_attachments" => [media_attachment]} = json_response(conn, 200)
  255. assert %{"type" => "image"} = media_attachment
  256. end
  257. test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now",
  258. %{conn: conn} do
  259. scheduled_at =
  260. NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(5) - 1, :millisecond)
  261. conn =
  262. post(conn, "/api/v1/statuses", %{
  263. "status" => "not scheduled",
  264. "scheduled_at" => scheduled_at
  265. })
  266. assert %{"content" => "not scheduled"} = json_response(conn, 200)
  267. assert [] == Repo.all(ScheduledActivity)
  268. end
  269. test "returns error when daily user limit is exceeded", %{user: user, conn: conn} do
  270. today =
  271. NaiveDateTime.utc_now()
  272. |> NaiveDateTime.add(:timer.minutes(6), :millisecond)
  273. |> NaiveDateTime.to_iso8601()
  274. attrs = %{params: %{}, scheduled_at: today}
  275. {:ok, _} = ScheduledActivity.create(user, attrs)
  276. {:ok, _} = ScheduledActivity.create(user, attrs)
  277. conn = post(conn, "/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => today})
  278. assert %{"error" => "daily limit exceeded"} == json_response(conn, 422)
  279. end
  280. test "returns error when total user limit is exceeded", %{user: user, conn: conn} do
  281. today =
  282. NaiveDateTime.utc_now()
  283. |> NaiveDateTime.add(:timer.minutes(6), :millisecond)
  284. |> NaiveDateTime.to_iso8601()
  285. tomorrow =
  286. NaiveDateTime.utc_now()
  287. |> NaiveDateTime.add(:timer.hours(36), :millisecond)
  288. |> NaiveDateTime.to_iso8601()
  289. attrs = %{params: %{}, scheduled_at: today}
  290. {:ok, _} = ScheduledActivity.create(user, attrs)
  291. {:ok, _} = ScheduledActivity.create(user, attrs)
  292. {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow})
  293. conn =
  294. post(conn, "/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => tomorrow})
  295. assert %{"error" => "total limit exceeded"} == json_response(conn, 422)
  296. end
  297. end
  298. describe "posting polls" do
  299. setup do: oauth_access(["write:statuses"])
  300. test "posting a poll", %{conn: conn} do
  301. time = NaiveDateTime.utc_now()
  302. conn =
  303. post(conn, "/api/v1/statuses", %{
  304. "status" => "Who is the #bestgrill?",
  305. "poll" => %{"options" => ["Rei", "Asuka", "Misato"], "expires_in" => 420}
  306. })
  307. response = json_response(conn, 200)
  308. assert Enum.all?(response["poll"]["options"], fn %{"title" => title} ->
  309. title in ["Rei", "Asuka", "Misato"]
  310. end)
  311. assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430
  312. refute response["poll"]["expred"]
  313. question = Object.get_by_id(response["poll"]["id"])
  314. # closed contains utc timezone
  315. assert question.data["closed"] =~ "Z"
  316. end
  317. test "option limit is enforced", %{conn: conn} do
  318. limit = Config.get([:instance, :poll_limits, :max_options])
  319. conn =
  320. post(conn, "/api/v1/statuses", %{
  321. "status" => "desu~",
  322. "poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1}
  323. })
  324. %{"error" => error} = json_response(conn, 422)
  325. assert error == "Poll can't contain more than #{limit} options"
  326. end
  327. test "option character limit is enforced", %{conn: conn} do
  328. limit = Config.get([:instance, :poll_limits, :max_option_chars])
  329. conn =
  330. post(conn, "/api/v1/statuses", %{
  331. "status" => "...",
  332. "poll" => %{
  333. "options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)],
  334. "expires_in" => 1
  335. }
  336. })
  337. %{"error" => error} = json_response(conn, 422)
  338. assert error == "Poll options cannot be longer than #{limit} characters each"
  339. end
  340. test "minimal date limit is enforced", %{conn: conn} do
  341. limit = Config.get([:instance, :poll_limits, :min_expiration])
  342. conn =
  343. post(conn, "/api/v1/statuses", %{
  344. "status" => "imagine arbitrary limits",
  345. "poll" => %{
  346. "options" => ["this post was made by pleroma gang"],
  347. "expires_in" => limit - 1
  348. }
  349. })
  350. %{"error" => error} = json_response(conn, 422)
  351. assert error == "Expiration date is too soon"
  352. end
  353. test "maximum date limit is enforced", %{conn: conn} do
  354. limit = Config.get([:instance, :poll_limits, :max_expiration])
  355. conn =
  356. post(conn, "/api/v1/statuses", %{
  357. "status" => "imagine arbitrary limits",
  358. "poll" => %{
  359. "options" => ["this post was made by pleroma gang"],
  360. "expires_in" => limit + 1
  361. }
  362. })
  363. %{"error" => error} = json_response(conn, 422)
  364. assert error == "Expiration date is too far in the future"
  365. end
  366. end
  367. test "get a status" do
  368. %{conn: conn} = oauth_access(["read:statuses"])
  369. activity = insert(:note_activity)
  370. conn = get(conn, "/api/v1/statuses/#{activity.id}")
  371. assert %{"id" => id} = json_response(conn, 200)
  372. assert id == to_string(activity.id)
  373. end
  374. test "getting a status that doesn't exist returns 404" do
  375. %{conn: conn} = oauth_access(["read:statuses"])
  376. activity = insert(:note_activity)
  377. conn = get(conn, "/api/v1/statuses/#{String.downcase(activity.id)}")
  378. assert json_response(conn, 404) == %{"error" => "Record not found"}
  379. end
  380. test "get a direct status" do
  381. %{user: user, conn: conn} = oauth_access(["read:statuses"])
  382. other_user = insert(:user)
  383. {:ok, activity} =
  384. CommonAPI.post(user, %{"status" => "@#{other_user.nickname}", "visibility" => "direct"})
  385. conn =
  386. conn
  387. |> assign(:user, user)
  388. |> get("/api/v1/statuses/#{activity.id}")
  389. [participation] = Participation.for_user(user)
  390. res = json_response(conn, 200)
  391. assert res["pleroma"]["direct_conversation_id"] == participation.id
  392. end
  393. test "get statuses by IDs" do
  394. %{conn: conn} = oauth_access(["read:statuses"])
  395. %{id: id1} = insert(:note_activity)
  396. %{id: id2} = insert(:note_activity)
  397. query_string = "ids[]=#{id1}&ids[]=#{id2}"
  398. conn = get(conn, "/api/v1/statuses/?#{query_string}")
  399. assert [%{"id" => ^id1}, %{"id" => ^id2}] = Enum.sort_by(json_response(conn, :ok), & &1["id"])
  400. end
  401. describe "deleting a status" do
  402. test "when you created it" do
  403. %{user: author, conn: conn} = oauth_access(["write:statuses"])
  404. activity = insert(:note_activity, user: author)
  405. conn =
  406. conn
  407. |> assign(:user, author)
  408. |> delete("/api/v1/statuses/#{activity.id}")
  409. assert %{} = json_response(conn, 200)
  410. refute Activity.get_by_id(activity.id)
  411. end
  412. test "when it doesn't exist" do
  413. %{user: author, conn: conn} = oauth_access(["write:statuses"])
  414. activity = insert(:note_activity, user: author)
  415. conn =
  416. conn
  417. |> assign(:user, author)
  418. |> delete("/api/v1/statuses/#{String.downcase(activity.id)}")
  419. assert %{"error" => "Record not found"} == json_response(conn, 404)
  420. end
  421. test "when you didn't create it" do
  422. %{conn: conn} = oauth_access(["write:statuses"])
  423. activity = insert(:note_activity)
  424. conn = delete(conn, "/api/v1/statuses/#{activity.id}")
  425. assert %{"error" => _} = json_response(conn, 403)
  426. assert Activity.get_by_id(activity.id) == activity
  427. end
  428. test "when you're an admin or moderator", %{conn: conn} do
  429. activity1 = insert(:note_activity)
  430. activity2 = insert(:note_activity)
  431. admin = insert(:user, is_admin: true)
  432. moderator = insert(:user, is_moderator: true)
  433. res_conn =
  434. conn
  435. |> assign(:user, admin)
  436. |> assign(:token, insert(:oauth_token, user: admin, scopes: ["write:statuses"]))
  437. |> delete("/api/v1/statuses/#{activity1.id}")
  438. assert %{} = json_response(res_conn, 200)
  439. res_conn =
  440. conn
  441. |> assign(:user, moderator)
  442. |> assign(:token, insert(:oauth_token, user: moderator, scopes: ["write:statuses"]))
  443. |> delete("/api/v1/statuses/#{activity2.id}")
  444. assert %{} = json_response(res_conn, 200)
  445. refute Activity.get_by_id(activity1.id)
  446. refute Activity.get_by_id(activity2.id)
  447. end
  448. end
  449. describe "reblogging" do
  450. setup do: oauth_access(["write:statuses"])
  451. test "reblogs and returns the reblogged status", %{conn: conn} do
  452. activity = insert(:note_activity)
  453. conn = post(conn, "/api/v1/statuses/#{activity.id}/reblog")
  454. assert %{
  455. "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
  456. "reblogged" => true
  457. } = json_response(conn, 200)
  458. assert to_string(activity.id) == id
  459. end
  460. test "returns 404 if the reblogged status doesn't exist", %{conn: conn} do
  461. activity = insert(:note_activity)
  462. conn = post(conn, "/api/v1/statuses/#{String.downcase(activity.id)}/reblog")
  463. assert %{"error" => "Record not found"} = json_response(conn, 404)
  464. end
  465. test "reblogs privately and returns the reblogged status", %{conn: conn} do
  466. activity = insert(:note_activity)
  467. conn = post(conn, "/api/v1/statuses/#{activity.id}/reblog", %{"visibility" => "private"})
  468. assert %{
  469. "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
  470. "reblogged" => true,
  471. "visibility" => "private"
  472. } = json_response(conn, 200)
  473. assert to_string(activity.id) == id
  474. end
  475. test "reblogged status for another user" do
  476. activity = insert(:note_activity)
  477. user1 = insert(:user)
  478. user2 = insert(:user)
  479. user3 = insert(:user)
  480. CommonAPI.favorite(activity.id, user2)
  481. {:ok, _bookmark} = Pleroma.Bookmark.create(user2.id, activity.id)
  482. {:ok, reblog_activity1, _object} = CommonAPI.repeat(activity.id, user1)
  483. {:ok, _, _object} = CommonAPI.repeat(activity.id, user2)
  484. conn_res =
  485. build_conn()
  486. |> assign(:user, user3)
  487. |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
  488. |> get("/api/v1/statuses/#{reblog_activity1.id}")
  489. assert %{
  490. "reblog" => %{"id" => id, "reblogged" => false, "reblogs_count" => 2},
  491. "reblogged" => false,
  492. "favourited" => false,
  493. "bookmarked" => false
  494. } = json_response(conn_res, 200)
  495. conn_res =
  496. build_conn()
  497. |> assign(:user, user2)
  498. |> assign(:token, insert(:oauth_token, user: user2, scopes: ["read:statuses"]))
  499. |> get("/api/v1/statuses/#{reblog_activity1.id}")
  500. assert %{
  501. "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 2},
  502. "reblogged" => true,
  503. "favourited" => true,
  504. "bookmarked" => true
  505. } = json_response(conn_res, 200)
  506. assert to_string(activity.id) == id
  507. end
  508. end
  509. describe "unreblogging" do
  510. setup do: oauth_access(["write:statuses"])
  511. test "unreblogs and returns the unreblogged status", %{user: user, conn: conn} do
  512. activity = insert(:note_activity)
  513. {:ok, _, _} = CommonAPI.repeat(activity.id, user)
  514. conn = post(conn, "/api/v1/statuses/#{activity.id}/unreblog")
  515. assert %{"id" => id, "reblogged" => false, "reblogs_count" => 0} = json_response(conn, 200)
  516. assert to_string(activity.id) == id
  517. end
  518. test "returns 404 error when activity does not exist", %{conn: conn} do
  519. conn = post(conn, "/api/v1/statuses/foo/unreblog")
  520. assert json_response(conn, 404) == %{"error" => "Record not found"}
  521. end
  522. end
  523. describe "favoriting" do
  524. setup do: oauth_access(["write:favourites"])
  525. test "favs a status and returns it", %{conn: conn} do
  526. activity = insert(:note_activity)
  527. conn = post(conn, "/api/v1/statuses/#{activity.id}/favourite")
  528. assert %{"id" => id, "favourites_count" => 1, "favourited" => true} =
  529. json_response(conn, 200)
  530. assert to_string(activity.id) == id
  531. end
  532. test "favoriting twice will just return 200", %{conn: conn} do
  533. activity = insert(:note_activity)
  534. post(conn, "/api/v1/statuses/#{activity.id}/favourite")
  535. assert post(conn, "/api/v1/statuses/#{activity.id}/favourite") |> json_response(200)
  536. end
  537. test "returns 404 error for a wrong id", %{conn: conn} do
  538. conn = post(conn, "/api/v1/statuses/1/favourite")
  539. assert json_response(conn, 404) == %{"error" => "Record not found"}
  540. end
  541. end
  542. describe "unfavoriting" do
  543. setup do: oauth_access(["write:favourites"])
  544. test "unfavorites a status and returns it", %{user: user, conn: conn} do
  545. activity = insert(:note_activity)
  546. {:ok, _, _} = CommonAPI.favorite(activity.id, user)
  547. conn = post(conn, "/api/v1/statuses/#{activity.id}/unfavourite")
  548. assert %{"id" => id, "favourites_count" => 0, "favourited" => false} =
  549. json_response(conn, 200)
  550. assert to_string(activity.id) == id
  551. end
  552. test "returns 404 error for a wrong id", %{conn: conn} do
  553. conn = post(conn, "/api/v1/statuses/1/unfavourite")
  554. assert json_response(conn, 404) == %{"error" => "Record not found"}
  555. end
  556. end
  557. describe "pinned statuses" do
  558. setup do: oauth_access(["write:accounts"])
  559. setup %{user: user} do
  560. {:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!"})
  561. %{activity: activity}
  562. end
  563. clear_config([:instance, :max_pinned_statuses]) do
  564. Config.put([:instance, :max_pinned_statuses], 1)
  565. end
  566. test "pin status", %{conn: conn, user: user, activity: activity} do
  567. id_str = to_string(activity.id)
  568. assert %{"id" => ^id_str, "pinned" => true} =
  569. conn
  570. |> post("/api/v1/statuses/#{activity.id}/pin")
  571. |> json_response(200)
  572. assert [%{"id" => ^id_str, "pinned" => true}] =
  573. conn
  574. |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
  575. |> json_response(200)
  576. end
  577. test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do
  578. {:ok, dm} = CommonAPI.post(user, %{"status" => "test", "visibility" => "direct"})
  579. conn = post(conn, "/api/v1/statuses/#{dm.id}/pin")
  580. assert json_response(conn, 400) == %{"error" => "Could not pin"}
  581. end
  582. test "unpin status", %{conn: conn, user: user, activity: activity} do
  583. {:ok, _} = CommonAPI.pin(activity.id, user)
  584. user = refresh_record(user)
  585. id_str = to_string(activity.id)
  586. assert %{"id" => ^id_str, "pinned" => false} =
  587. conn
  588. |> assign(:user, user)
  589. |> post("/api/v1/statuses/#{activity.id}/unpin")
  590. |> json_response(200)
  591. assert [] =
  592. conn
  593. |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
  594. |> json_response(200)
  595. end
  596. test "/unpin: returns 400 error when activity is not exist", %{conn: conn} do
  597. conn = post(conn, "/api/v1/statuses/1/unpin")
  598. assert json_response(conn, 400) == %{"error" => "Could not unpin"}
  599. end
  600. test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do
  601. {:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"})
  602. id_str_one = to_string(activity_one.id)
  603. assert %{"id" => ^id_str_one, "pinned" => true} =
  604. conn
  605. |> post("/api/v1/statuses/#{id_str_one}/pin")
  606. |> json_response(200)
  607. user = refresh_record(user)
  608. assert %{"error" => "You have already pinned the maximum number of statuses"} =
  609. conn
  610. |> assign(:user, user)
  611. |> post("/api/v1/statuses/#{activity_two.id}/pin")
  612. |> json_response(400)
  613. end
  614. end
  615. describe "cards" do
  616. setup do
  617. Config.put([:rich_media, :enabled], true)
  618. oauth_access(["read:statuses"])
  619. end
  620. test "returns rich-media card", %{conn: conn, user: user} do
  621. Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
  622. {:ok, activity} = CommonAPI.post(user, %{"status" => "https://example.com/ogp"})
  623. card_data = %{
  624. "image" => "http://ia.media-imdb.com/images/rock.jpg",
  625. "provider_name" => "example.com",
  626. "provider_url" => "https://example.com",
  627. "title" => "The Rock",
  628. "type" => "link",
  629. "url" => "https://example.com/ogp",
  630. "description" =>
  631. "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.",
  632. "pleroma" => %{
  633. "opengraph" => %{
  634. "image" => "http://ia.media-imdb.com/images/rock.jpg",
  635. "title" => "The Rock",
  636. "type" => "video.movie",
  637. "url" => "https://example.com/ogp",
  638. "description" =>
  639. "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer."
  640. }
  641. }
  642. }
  643. response =
  644. conn
  645. |> get("/api/v1/statuses/#{activity.id}/card")
  646. |> json_response(200)
  647. assert response == card_data
  648. # works with private posts
  649. {:ok, activity} =
  650. CommonAPI.post(user, %{"status" => "https://example.com/ogp", "visibility" => "direct"})
  651. response_two =
  652. conn
  653. |> get("/api/v1/statuses/#{activity.id}/card")
  654. |> json_response(200)
  655. assert response_two == card_data
  656. end
  657. test "replaces missing description with an empty string", %{conn: conn, user: user} do
  658. Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
  659. {:ok, activity} =
  660. CommonAPI.post(user, %{"status" => "https://example.com/ogp-missing-data"})
  661. response =
  662. conn
  663. |> get("/api/v1/statuses/#{activity.id}/card")
  664. |> json_response(:ok)
  665. assert response == %{
  666. "type" => "link",
  667. "title" => "Pleroma",
  668. "description" => "",
  669. "image" => nil,
  670. "provider_name" => "example.com",
  671. "provider_url" => "https://example.com",
  672. "url" => "https://example.com/ogp-missing-data",
  673. "pleroma" => %{
  674. "opengraph" => %{
  675. "title" => "Pleroma",
  676. "type" => "website",
  677. "url" => "https://example.com/ogp-missing-data"
  678. }
  679. }
  680. }
  681. end
  682. end
  683. test "bookmarks" do
  684. %{conn: conn} = oauth_access(["write:bookmarks", "read:bookmarks"])
  685. author = insert(:user)
  686. {:ok, activity1} =
  687. CommonAPI.post(author, %{
  688. "status" => "heweoo?"
  689. })
  690. {:ok, activity2} =
  691. CommonAPI.post(author, %{
  692. "status" => "heweoo!"
  693. })
  694. response1 = post(conn, "/api/v1/statuses/#{activity1.id}/bookmark")
  695. assert json_response(response1, 200)["bookmarked"] == true
  696. response2 = post(conn, "/api/v1/statuses/#{activity2.id}/bookmark")
  697. assert json_response(response2, 200)["bookmarked"] == true
  698. bookmarks = get(conn, "/api/v1/bookmarks")
  699. assert [json_response(response2, 200), json_response(response1, 200)] ==
  700. json_response(bookmarks, 200)
  701. response1 = post(conn, "/api/v1/statuses/#{activity1.id}/unbookmark")
  702. assert json_response(response1, 200)["bookmarked"] == false
  703. bookmarks = get(conn, "/api/v1/bookmarks")
  704. assert [json_response(response2, 200)] == json_response(bookmarks, 200)
  705. end
  706. describe "conversation muting" do
  707. setup do: oauth_access(["write:mutes"])
  708. setup do
  709. post_user = insert(:user)
  710. {:ok, activity} = CommonAPI.post(post_user, %{"status" => "HIE"})
  711. %{activity: activity}
  712. end
  713. test "mute conversation", %{conn: conn, activity: activity} do
  714. id_str = to_string(activity.id)
  715. assert %{"id" => ^id_str, "muted" => true} =
  716. conn
  717. |> post("/api/v1/statuses/#{activity.id}/mute")
  718. |> json_response(200)
  719. end
  720. test "cannot mute already muted conversation", %{conn: conn, user: user, activity: activity} do
  721. {:ok, _} = CommonAPI.add_mute(user, activity)
  722. conn = post(conn, "/api/v1/statuses/#{activity.id}/mute")
  723. assert json_response(conn, 400) == %{"error" => "conversation is already muted"}
  724. end
  725. test "unmute conversation", %{conn: conn, user: user, activity: activity} do
  726. {:ok, _} = CommonAPI.add_mute(user, activity)
  727. id_str = to_string(activity.id)
  728. assert %{"id" => ^id_str, "muted" => false} =
  729. conn
  730. # |> assign(:user, user)
  731. |> post("/api/v1/statuses/#{activity.id}/unmute")
  732. |> json_response(200)
  733. end
  734. end
  735. test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{conn: conn} do
  736. user1 = insert(:user)
  737. user2 = insert(:user)
  738. user3 = insert(:user)
  739. {:ok, replied_to} = CommonAPI.post(user1, %{"status" => "cofe"})
  740. # Reply to status from another user
  741. conn1 =
  742. conn
  743. |> assign(:user, user2)
  744. |> assign(:token, insert(:oauth_token, user: user2, scopes: ["write:statuses"]))
  745. |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
  746. assert %{"content" => "xD", "id" => id} = json_response(conn1, 200)
  747. activity = Activity.get_by_id_with_object(id)
  748. assert Object.normalize(activity).data["inReplyTo"] == Object.normalize(replied_to).data["id"]
  749. assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
  750. # Reblog from the third user
  751. conn2 =
  752. conn
  753. |> assign(:user, user3)
  754. |> assign(:token, insert(:oauth_token, user: user3, scopes: ["write:statuses"]))
  755. |> post("/api/v1/statuses/#{activity.id}/reblog")
  756. assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} =
  757. json_response(conn2, 200)
  758. assert to_string(activity.id) == id
  759. # Getting third user status
  760. conn3 =
  761. conn
  762. |> assign(:user, user3)
  763. |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
  764. |> get("api/v1/timelines/home")
  765. [reblogged_activity] = json_response(conn3, 200)
  766. assert reblogged_activity["reblog"]["in_reply_to_id"] == replied_to.id
  767. replied_to_user = User.get_by_ap_id(replied_to.data["actor"])
  768. assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id
  769. end
  770. describe "GET /api/v1/statuses/:id/favourited_by" do
  771. setup do: oauth_access(["read:accounts"])
  772. setup %{user: user} do
  773. {:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
  774. %{activity: activity}
  775. end
  776. test "returns users who have favorited the status", %{conn: conn, activity: activity} do
  777. other_user = insert(:user)
  778. {:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
  779. response =
  780. conn
  781. |> get("/api/v1/statuses/#{activity.id}/favourited_by")
  782. |> json_response(:ok)
  783. [%{"id" => id}] = response
  784. assert id == other_user.id
  785. end
  786. test "returns empty array when status has not been favorited yet", %{
  787. conn: conn,
  788. activity: activity
  789. } do
  790. response =
  791. conn
  792. |> get("/api/v1/statuses/#{activity.id}/favourited_by")
  793. |> json_response(:ok)
  794. assert Enum.empty?(response)
  795. end
  796. test "does not return users who have favorited the status but are blocked", %{
  797. conn: %{assigns: %{user: user}} = conn,
  798. activity: activity
  799. } do
  800. other_user = insert(:user)
  801. {:ok, _user_relationship} = User.block(user, other_user)
  802. {:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
  803. response =
  804. conn
  805. |> get("/api/v1/statuses/#{activity.id}/favourited_by")
  806. |> json_response(:ok)
  807. assert Enum.empty?(response)
  808. end
  809. test "does not fail on an unauthenticated request", %{activity: activity} do
  810. other_user = insert(:user)
  811. {:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
  812. response =
  813. build_conn()
  814. |> get("/api/v1/statuses/#{activity.id}/favourited_by")
  815. |> json_response(:ok)
  816. [%{"id" => id}] = response
  817. assert id == other_user.id
  818. end
  819. test "requires authentication for private posts", %{user: user} do
  820. other_user = insert(:user)
  821. {:ok, activity} =
  822. CommonAPI.post(user, %{
  823. "status" => "@#{other_user.nickname} wanna get some #cofe together?",
  824. "visibility" => "direct"
  825. })
  826. {:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
  827. favourited_by_url = "/api/v1/statuses/#{activity.id}/favourited_by"
  828. build_conn()
  829. |> get(favourited_by_url)
  830. |> json_response(404)
  831. conn =
  832. build_conn()
  833. |> assign(:user, other_user)
  834. |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
  835. conn
  836. |> assign(:token, nil)
  837. |> get(favourited_by_url)
  838. |> json_response(404)
  839. response =
  840. conn
  841. |> get(favourited_by_url)
  842. |> json_response(200)
  843. [%{"id" => id}] = response
  844. assert id == other_user.id
  845. end
  846. end
  847. describe "GET /api/v1/statuses/:id/reblogged_by" do
  848. setup do: oauth_access(["read:accounts"])
  849. setup %{user: user} do
  850. {:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
  851. %{activity: activity}
  852. end
  853. test "returns users who have reblogged the status", %{conn: conn, activity: activity} do
  854. other_user = insert(:user)
  855. {:ok, _, _} = CommonAPI.repeat(activity.id, other_user)
  856. response =
  857. conn
  858. |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
  859. |> json_response(:ok)
  860. [%{"id" => id}] = response
  861. assert id == other_user.id
  862. end
  863. test "returns empty array when status has not been reblogged yet", %{
  864. conn: conn,
  865. activity: activity
  866. } do
  867. response =
  868. conn
  869. |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
  870. |> json_response(:ok)
  871. assert Enum.empty?(response)
  872. end
  873. test "does not return users who have reblogged the status but are blocked", %{
  874. conn: %{assigns: %{user: user}} = conn,
  875. activity: activity
  876. } do
  877. other_user = insert(:user)
  878. {:ok, _user_relationship} = User.block(user, other_user)
  879. {:ok, _, _} = CommonAPI.repeat(activity.id, other_user)
  880. response =
  881. conn
  882. |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
  883. |> json_response(:ok)
  884. assert Enum.empty?(response)
  885. end
  886. test "does not return users who have reblogged the status privately", %{
  887. conn: conn,
  888. activity: activity
  889. } do
  890. other_user = insert(:user)
  891. {:ok, _, _} = CommonAPI.repeat(activity.id, other_user, %{"visibility" => "private"})
  892. response =
  893. conn
  894. |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
  895. |> json_response(:ok)
  896. assert Enum.empty?(response)
  897. end
  898. test "does not fail on an unauthenticated request", %{activity: activity} do
  899. other_user = insert(:user)
  900. {:ok, _, _} = CommonAPI.repeat(activity.id, other_user)
  901. response =
  902. build_conn()
  903. |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
  904. |> json_response(:ok)
  905. [%{"id" => id}] = response
  906. assert id == other_user.id
  907. end
  908. test "requires authentication for private posts", %{user: user} do
  909. other_user = insert(:user)
  910. {:ok, activity} =
  911. CommonAPI.post(user, %{
  912. "status" => "@#{other_user.nickname} wanna get some #cofe together?",
  913. "visibility" => "direct"
  914. })
  915. build_conn()
  916. |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
  917. |> json_response(404)
  918. response =
  919. build_conn()
  920. |> assign(:user, other_user)
  921. |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
  922. |> get("/api/v1/statuses/#{activity.id}/reblogged_by")
  923. |> json_response(200)
  924. assert [] == response
  925. end
  926. end
  927. test "context" do
  928. user = insert(:user)
  929. {:ok, %{id: id1}} = CommonAPI.post(user, %{"status" => "1"})
  930. {:ok, %{id: id2}} = CommonAPI.post(user, %{"status" => "2", "in_reply_to_status_id" => id1})
  931. {:ok, %{id: id3}} = CommonAPI.post(user, %{"status" => "3", "in_reply_to_status_id" => id2})
  932. {:ok, %{id: id4}} = CommonAPI.post(user, %{"status" => "4", "in_reply_to_status_id" => id3})
  933. {:ok, %{id: id5}} = CommonAPI.post(user, %{"status" => "5", "in_reply_to_status_id" => id4})
  934. response =
  935. build_conn()
  936. |> get("/api/v1/statuses/#{id3}/context")
  937. |> json_response(:ok)
  938. assert %{
  939. "ancestors" => [%{"id" => ^id1}, %{"id" => ^id2}],
  940. "descendants" => [%{"id" => ^id4}, %{"id" => ^id5}]
  941. } = response
  942. end
  943. test "returns the favorites of a user" do
  944. %{user: user, conn: conn} = oauth_access(["read:favourites"])
  945. other_user = insert(:user)
  946. {:ok, _} = CommonAPI.post(other_user, %{"status" => "bla"})
  947. {:ok, activity} = CommonAPI.post(other_user, %{"status" => "traps are happy"})
  948. {:ok, _, _} = CommonAPI.favorite(activity.id, user)
  949. first_conn = get(conn, "/api/v1/favourites")
  950. assert [status] = json_response(first_conn, 200)
  951. assert status["id"] == to_string(activity.id)
  952. assert [{"link", _link_header}] =
  953. Enum.filter(first_conn.resp_headers, fn element -> match?({"link", _}, element) end)
  954. # Honours query params
  955. {:ok, second_activity} =
  956. CommonAPI.post(other_user, %{
  957. "status" =>
  958. "Trees Are Never Sad Look At Them Every Once In Awhile They're Quite Beautiful."
  959. })
  960. {:ok, _, _} = CommonAPI.favorite(second_activity.id, user)
  961. last_like = status["id"]
  962. second_conn = get(conn, "/api/v1/favourites?since_id=#{last_like}")
  963. assert [second_status] = json_response(second_conn, 200)
  964. assert second_status["id"] == to_string(second_activity.id)
  965. third_conn = get(conn, "/api/v1/favourites?limit=0")
  966. assert [] = json_response(third_conn, 200)
  967. end
  968. test "expires_at is nil for another user" do
  969. %{conn: conn, user: user} = oauth_access(["read:statuses"])
  970. {:ok, activity} = CommonAPI.post(user, %{"status" => "foobar", "expires_in" => 1_000_000})
  971. expires_at =
  972. activity.id
  973. |> ActivityExpiration.get_by_activity_id()
  974. |> Map.get(:scheduled_at)
  975. |> NaiveDateTime.to_iso8601()
  976. assert %{"pleroma" => %{"expires_at" => ^expires_at}} =
  977. conn |> get("/api/v1/statuses/#{activity.id}") |> json_response(:ok)
  978. %{conn: conn} = oauth_access(["read:statuses"])
  979. assert %{"pleroma" => %{"expires_at" => nil}} =
  980. conn |> get("/api/v1/statuses/#{activity.id}") |> json_response(:ok)
  981. end
  982. end