Merge branch 'feature/move-activity' into 'develop'
[akkoma] / test / web / activity_pub / transmogrifier_test.exs
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
6 use Pleroma.DataCase
7 alias Pleroma.Activity
8 alias Pleroma.Object
9 alias Pleroma.Object.Fetcher
10 alias Pleroma.Tests.ObanHelpers
11 alias Pleroma.User
12 alias Pleroma.Web.ActivityPub.ActivityPub
13 alias Pleroma.Web.ActivityPub.Transmogrifier
14 alias Pleroma.Web.AdminAPI.AccountView
15 alias Pleroma.Web.CommonAPI
16
17 import Mock
18 import Pleroma.Factory
19 import ExUnit.CaptureLog
20
21 setup_all do
22 Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
23 :ok
24 end
25
26 clear_config([:instance, :max_remote_account_fields])
27
28 describe "handle_incoming" do
29 test "it ignores an incoming notice if we already have it" do
30 activity = insert(:note_activity)
31
32 data =
33 File.read!("test/fixtures/mastodon-post-activity.json")
34 |> Poison.decode!()
35 |> Map.put("object", Object.normalize(activity).data)
36
37 {:ok, returned_activity} = Transmogrifier.handle_incoming(data)
38
39 assert activity == returned_activity
40 end
41
42 @tag capture_log: true
43 test "it fetches replied-to activities if we don't have them" do
44 data =
45 File.read!("test/fixtures/mastodon-post-activity.json")
46 |> Poison.decode!()
47
48 object =
49 data["object"]
50 |> Map.put("inReplyTo", "https://shitposter.club/notice/2827873")
51
52 data = Map.put(data, "object", object)
53 {:ok, returned_activity} = Transmogrifier.handle_incoming(data)
54 returned_object = Object.normalize(returned_activity, false)
55
56 assert activity =
57 Activity.get_create_by_object_ap_id(
58 "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
59 )
60
61 assert returned_object.data["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
62 end
63
64 test "it does not fetch replied-to activities beyond max_replies_depth" do
65 data =
66 File.read!("test/fixtures/mastodon-post-activity.json")
67 |> Poison.decode!()
68
69 object =
70 data["object"]
71 |> Map.put("inReplyTo", "https://shitposter.club/notice/2827873")
72
73 data = Map.put(data, "object", object)
74
75 with_mock Pleroma.Web.Federator,
76 allowed_incoming_reply_depth?: fn _ -> false end do
77 {:ok, returned_activity} = Transmogrifier.handle_incoming(data)
78
79 returned_object = Object.normalize(returned_activity, false)
80
81 refute Activity.get_create_by_object_ap_id(
82 "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
83 )
84
85 assert returned_object.data["inReplyToAtomUri"] ==
86 "https://shitposter.club/notice/2827873"
87 end
88 end
89
90 test "it does not crash if the object in inReplyTo can't be fetched" do
91 data =
92 File.read!("test/fixtures/mastodon-post-activity.json")
93 |> Poison.decode!()
94
95 object =
96 data["object"]
97 |> Map.put("inReplyTo", "https://404.site/whatever")
98
99 data =
100 data
101 |> Map.put("object", object)
102
103 assert capture_log(fn ->
104 {:ok, _returned_activity} = Transmogrifier.handle_incoming(data)
105 end) =~ "[error] Couldn't fetch \"https://404.site/whatever\", error: nil"
106 end
107
108 test "it works for incoming notices" do
109 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
110
111 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
112
113 assert data["id"] ==
114 "http://mastodon.example.org/users/admin/statuses/99512778738411822/activity"
115
116 assert data["context"] ==
117 "tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation"
118
119 assert data["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
120
121 assert data["cc"] == [
122 "http://mastodon.example.org/users/admin/followers",
123 "http://localtesting.pleroma.lol/users/lain"
124 ]
125
126 assert data["actor"] == "http://mastodon.example.org/users/admin"
127
128 object_data = Object.normalize(data["object"]).data
129
130 assert object_data["id"] ==
131 "http://mastodon.example.org/users/admin/statuses/99512778738411822"
132
133 assert object_data["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
134
135 assert object_data["cc"] == [
136 "http://mastodon.example.org/users/admin/followers",
137 "http://localtesting.pleroma.lol/users/lain"
138 ]
139
140 assert object_data["actor"] == "http://mastodon.example.org/users/admin"
141 assert object_data["attributedTo"] == "http://mastodon.example.org/users/admin"
142
143 assert object_data["context"] ==
144 "tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation"
145
146 assert object_data["sensitive"] == true
147
148 user = User.get_cached_by_ap_id(object_data["actor"])
149
150 assert user.note_count == 1
151 end
152
153 test "it works for incoming notices with hashtags" do
154 data = File.read!("test/fixtures/mastodon-post-activity-hashtag.json") |> Poison.decode!()
155
156 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
157 object = Object.normalize(data["object"])
158
159 assert Enum.at(object.data["tag"], 2) == "moo"
160 end
161
162 test "it works for incoming questions" do
163 data = File.read!("test/fixtures/mastodon-question-activity.json") |> Poison.decode!()
164
165 {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
166
167 object = Object.normalize(activity)
168
169 assert Enum.all?(object.data["oneOf"], fn choice ->
170 choice["name"] in [
171 "Dunno",
172 "Everyone knows that!",
173 "25 char limit is dumb",
174 "I can't even fit a funny"
175 ]
176 end)
177 end
178
179 test "it works for incoming listens" do
180 data = %{
181 "@context" => "https://www.w3.org/ns/activitystreams",
182 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
183 "cc" => [],
184 "type" => "Listen",
185 "id" => "http://mastodon.example.org/users/admin/listens/1234/activity",
186 "actor" => "http://mastodon.example.org/users/admin",
187 "object" => %{
188 "type" => "Audio",
189 "id" => "http://mastodon.example.org/users/admin/listens/1234",
190 "attributedTo" => "http://mastodon.example.org/users/admin",
191 "title" => "lain radio episode 1",
192 "artist" => "lain",
193 "album" => "lain radio",
194 "length" => 180_000
195 }
196 }
197
198 {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
199
200 object = Object.normalize(activity)
201
202 assert object.data["title"] == "lain radio episode 1"
203 assert object.data["artist"] == "lain"
204 assert object.data["album"] == "lain radio"
205 assert object.data["length"] == 180_000
206 end
207
208 test "it rewrites Note votes to Answers and increments vote counters on question activities" do
209 user = insert(:user)
210
211 {:ok, activity} =
212 CommonAPI.post(user, %{
213 "status" => "suya...",
214 "poll" => %{"options" => ["suya", "suya.", "suya.."], "expires_in" => 10}
215 })
216
217 object = Object.normalize(activity)
218
219 data =
220 File.read!("test/fixtures/mastodon-vote.json")
221 |> Poison.decode!()
222 |> Kernel.put_in(["to"], user.ap_id)
223 |> Kernel.put_in(["object", "inReplyTo"], object.data["id"])
224 |> Kernel.put_in(["object", "to"], user.ap_id)
225
226 {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
227 answer_object = Object.normalize(activity)
228 assert answer_object.data["type"] == "Answer"
229 object = Object.get_by_ap_id(object.data["id"])
230
231 assert Enum.any?(
232 object.data["oneOf"],
233 fn
234 %{"name" => "suya..", "replies" => %{"totalItems" => 1}} -> true
235 _ -> false
236 end
237 )
238 end
239
240 test "it works for incoming notices with contentMap" do
241 data =
242 File.read!("test/fixtures/mastodon-post-activity-contentmap.json") |> Poison.decode!()
243
244 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
245 object = Object.normalize(data["object"])
246
247 assert object.data["content"] ==
248 "<p><span class=\"h-card\"><a href=\"http://localtesting.pleroma.lol/users/lain\" class=\"u-url mention\">@<span>lain</span></a></span></p>"
249 end
250
251 test "it works for incoming notices with to/cc not being an array (kroeg)" do
252 data = File.read!("test/fixtures/kroeg-post-activity.json") |> Poison.decode!()
253
254 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
255 object = Object.normalize(data["object"])
256
257 assert object.data["content"] ==
258 "<p>henlo from my Psion netBook</p><p>message sent from my Psion netBook</p>"
259 end
260
261 test "it works for incoming announces with actor being inlined (kroeg)" do
262 data = File.read!("test/fixtures/kroeg-announce-with-inline-actor.json") |> Poison.decode!()
263
264 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
265
266 assert data["actor"] == "https://puckipedia.com/"
267 end
268
269 test "it works for incoming notices with tag not being an array (kroeg)" do
270 data = File.read!("test/fixtures/kroeg-array-less-emoji.json") |> Poison.decode!()
271
272 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
273 object = Object.normalize(data["object"])
274
275 assert object.data["emoji"] == %{
276 "icon_e_smile" => "https://puckipedia.com/forum/images/smilies/icon_e_smile.png"
277 }
278
279 data = File.read!("test/fixtures/kroeg-array-less-hashtag.json") |> Poison.decode!()
280
281 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
282 object = Object.normalize(data["object"])
283
284 assert "test" in object.data["tag"]
285 end
286
287 test "it works for incoming notices with url not being a string (prismo)" do
288 data = File.read!("test/fixtures/prismo-url-map.json") |> Poison.decode!()
289
290 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
291 object = Object.normalize(data["object"])
292
293 assert object.data["url"] == "https://prismo.news/posts/83"
294 end
295
296 test "it cleans up incoming notices which are not really DMs" do
297 user = insert(:user)
298 other_user = insert(:user)
299
300 to = [user.ap_id, other_user.ap_id]
301
302 data =
303 File.read!("test/fixtures/mastodon-post-activity.json")
304 |> Poison.decode!()
305 |> Map.put("to", to)
306 |> Map.put("cc", [])
307
308 object =
309 data["object"]
310 |> Map.put("to", to)
311 |> Map.put("cc", [])
312
313 data = Map.put(data, "object", object)
314
315 {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data)
316
317 assert data["to"] == []
318 assert data["cc"] == to
319
320 object_data = Object.normalize(activity).data
321
322 assert object_data["to"] == []
323 assert object_data["cc"] == to
324 end
325
326 test "it works for incoming likes" do
327 user = insert(:user)
328 {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
329
330 data =
331 File.read!("test/fixtures/mastodon-like.json")
332 |> Poison.decode!()
333 |> Map.put("object", activity.data["object"])
334
335 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
336
337 assert data["actor"] == "http://mastodon.example.org/users/admin"
338 assert data["type"] == "Like"
339 assert data["id"] == "http://mastodon.example.org/users/admin#likes/2"
340 assert data["object"] == activity.data["object"]
341 end
342
343 test "it works for incoming misskey likes, turning them into EmojiReactions" do
344 user = insert(:user)
345 {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
346
347 data =
348 File.read!("test/fixtures/misskey-like.json")
349 |> Poison.decode!()
350 |> Map.put("object", activity.data["object"])
351
352 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
353
354 assert data["actor"] == data["actor"]
355 assert data["type"] == "EmojiReaction"
356 assert data["id"] == data["id"]
357 assert data["object"] == activity.data["object"]
358 assert data["content"] == "🍮"
359 end
360
361 test "it works for incoming misskey likes that contain unicode emojis, turning them into EmojiReactions" do
362 user = insert(:user)
363 {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
364
365 data =
366 File.read!("test/fixtures/misskey-like.json")
367 |> Poison.decode!()
368 |> Map.put("object", activity.data["object"])
369 |> Map.put("_misskey_reaction", "⭐")
370
371 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
372
373 assert data["actor"] == data["actor"]
374 assert data["type"] == "EmojiReaction"
375 assert data["id"] == data["id"]
376 assert data["object"] == activity.data["object"]
377 assert data["content"] == "⭐"
378 end
379
380 test "it works for incoming emoji reactions" do
381 user = insert(:user)
382 {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
383
384 data =
385 File.read!("test/fixtures/emoji-reaction.json")
386 |> Poison.decode!()
387 |> Map.put("object", activity.data["object"])
388
389 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
390
391 assert data["actor"] == "http://mastodon.example.org/users/admin"
392 assert data["type"] == "EmojiReaction"
393 assert data["id"] == "http://mastodon.example.org/users/admin#reactions/2"
394 assert data["object"] == activity.data["object"]
395 assert data["content"] == "👌"
396 end
397
398 test "it works for incoming emoji reaction undos" do
399 user = insert(:user)
400
401 {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
402 {:ok, reaction_activity, _object} = CommonAPI.react_with_emoji(activity.id, user, "👌")
403
404 data =
405 File.read!("test/fixtures/mastodon-undo-like.json")
406 |> Poison.decode!()
407 |> Map.put("object", reaction_activity.data["id"])
408 |> Map.put("actor", user.ap_id)
409
410 {:ok, activity} = Transmogrifier.handle_incoming(data)
411
412 assert activity.actor == user.ap_id
413 assert activity.data["id"] == data["id"]
414 assert activity.data["type"] == "Undo"
415 end
416
417 test "it returns an error for incoming unlikes wihout a like activity" do
418 user = insert(:user)
419 {:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
420
421 data =
422 File.read!("test/fixtures/mastodon-undo-like.json")
423 |> Poison.decode!()
424 |> Map.put("object", activity.data["object"])
425
426 assert Transmogrifier.handle_incoming(data) == :error
427 end
428
429 test "it works for incoming unlikes with an existing like activity" do
430 user = insert(:user)
431 {:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
432
433 like_data =
434 File.read!("test/fixtures/mastodon-like.json")
435 |> Poison.decode!()
436 |> Map.put("object", activity.data["object"])
437
438 {:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data)
439
440 data =
441 File.read!("test/fixtures/mastodon-undo-like.json")
442 |> Poison.decode!()
443 |> Map.put("object", like_data)
444 |> Map.put("actor", like_data["actor"])
445
446 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
447
448 assert data["actor"] == "http://mastodon.example.org/users/admin"
449 assert data["type"] == "Undo"
450 assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo"
451 assert data["object"]["id"] == "http://mastodon.example.org/users/admin#likes/2"
452 end
453
454 test "it works for incoming unlikes with an existing like activity and a compact object" do
455 user = insert(:user)
456 {:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
457
458 like_data =
459 File.read!("test/fixtures/mastodon-like.json")
460 |> Poison.decode!()
461 |> Map.put("object", activity.data["object"])
462
463 {:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data)
464
465 data =
466 File.read!("test/fixtures/mastodon-undo-like.json")
467 |> Poison.decode!()
468 |> Map.put("object", like_data["id"])
469 |> Map.put("actor", like_data["actor"])
470
471 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
472
473 assert data["actor"] == "http://mastodon.example.org/users/admin"
474 assert data["type"] == "Undo"
475 assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo"
476 assert data["object"]["id"] == "http://mastodon.example.org/users/admin#likes/2"
477 end
478
479 test "it works for incoming announces" do
480 data = File.read!("test/fixtures/mastodon-announce.json") |> Poison.decode!()
481
482 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
483
484 assert data["actor"] == "http://mastodon.example.org/users/admin"
485 assert data["type"] == "Announce"
486
487 assert data["id"] ==
488 "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
489
490 assert data["object"] ==
491 "http://mastodon.example.org/users/admin/statuses/99541947525187367"
492
493 assert Activity.get_create_by_object_ap_id(data["object"])
494 end
495
496 test "it works for incoming announces with an existing activity" do
497 user = insert(:user)
498 {:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
499
500 data =
501 File.read!("test/fixtures/mastodon-announce.json")
502 |> Poison.decode!()
503 |> Map.put("object", activity.data["object"])
504
505 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
506
507 assert data["actor"] == "http://mastodon.example.org/users/admin"
508 assert data["type"] == "Announce"
509
510 assert data["id"] ==
511 "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
512
513 assert data["object"] == activity.data["object"]
514
515 assert Activity.get_create_by_object_ap_id(data["object"]).id == activity.id
516 end
517
518 test "it works for incoming announces with an inlined activity" do
519 data =
520 File.read!("test/fixtures/mastodon-announce-private.json")
521 |> Poison.decode!()
522
523 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
524
525 assert data["actor"] == "http://mastodon.example.org/users/admin"
526 assert data["type"] == "Announce"
527
528 assert data["id"] ==
529 "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
530
531 object = Object.normalize(data["object"])
532
533 assert object.data["id"] == "http://mastodon.example.org/@admin/99541947525187368"
534 assert object.data["content"] == "this is a private toot"
535 end
536
537 @tag capture_log: true
538 test "it rejects incoming announces with an inlined activity from another origin" do
539 data =
540 File.read!("test/fixtures/bogus-mastodon-announce.json")
541 |> Poison.decode!()
542
543 assert :error = Transmogrifier.handle_incoming(data)
544 end
545
546 test "it does not clobber the addressing on announce activities" do
547 user = insert(:user)
548 {:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
549
550 data =
551 File.read!("test/fixtures/mastodon-announce.json")
552 |> Poison.decode!()
553 |> Map.put("object", Object.normalize(activity).data["id"])
554 |> Map.put("to", ["http://mastodon.example.org/users/admin/followers"])
555 |> Map.put("cc", [])
556
557 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
558
559 assert data["to"] == ["http://mastodon.example.org/users/admin/followers"]
560 end
561
562 test "it ensures that as:Public activities make it to their followers collection" do
563 user = insert(:user)
564
565 data =
566 File.read!("test/fixtures/mastodon-post-activity.json")
567 |> Poison.decode!()
568 |> Map.put("actor", user.ap_id)
569 |> Map.put("to", ["https://www.w3.org/ns/activitystreams#Public"])
570 |> Map.put("cc", [])
571
572 object =
573 data["object"]
574 |> Map.put("attributedTo", user.ap_id)
575 |> Map.put("to", ["https://www.w3.org/ns/activitystreams#Public"])
576 |> Map.put("cc", [])
577 |> Map.put("id", user.ap_id <> "/activities/12345678")
578
579 data = Map.put(data, "object", object)
580
581 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
582
583 assert data["cc"] == [User.ap_followers(user)]
584 end
585
586 test "it ensures that address fields become lists" do
587 user = insert(:user)
588
589 data =
590 File.read!("test/fixtures/mastodon-post-activity.json")
591 |> Poison.decode!()
592 |> Map.put("actor", user.ap_id)
593 |> Map.put("to", nil)
594 |> Map.put("cc", nil)
595
596 object =
597 data["object"]
598 |> Map.put("attributedTo", user.ap_id)
599 |> Map.put("to", nil)
600 |> Map.put("cc", nil)
601 |> Map.put("id", user.ap_id <> "/activities/12345678")
602
603 data = Map.put(data, "object", object)
604
605 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
606
607 assert !is_nil(data["to"])
608 assert !is_nil(data["cc"])
609 end
610
611 test "it strips internal likes" do
612 data =
613 File.read!("test/fixtures/mastodon-post-activity.json")
614 |> Poison.decode!()
615
616 likes = %{
617 "first" =>
618 "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes?page=1",
619 "id" => "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes",
620 "totalItems" => 3,
621 "type" => "OrderedCollection"
622 }
623
624 object = Map.put(data["object"], "likes", likes)
625 data = Map.put(data, "object", object)
626
627 {:ok, %Activity{object: object}} = Transmogrifier.handle_incoming(data)
628
629 refute Map.has_key?(object.data, "likes")
630 end
631
632 test "it strips internal reactions" do
633 user = insert(:user)
634 {:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"})
635 {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, user, "📢")
636
637 %{object: object} = Activity.get_by_id_with_object(activity.id)
638 assert Map.has_key?(object.data, "reactions")
639 assert Map.has_key?(object.data, "reaction_count")
640
641 object_data = Transmogrifier.strip_internal_fields(object.data)
642 refute Map.has_key?(object_data, "reactions")
643 refute Map.has_key?(object_data, "reaction_count")
644 end
645
646 test "it works for incoming update activities" do
647 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
648
649 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
650 update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!()
651
652 object =
653 update_data["object"]
654 |> Map.put("actor", data["actor"])
655 |> Map.put("id", data["actor"])
656
657 update_data =
658 update_data
659 |> Map.put("actor", data["actor"])
660 |> Map.put("object", object)
661
662 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data)
663
664 assert data["id"] == update_data["id"]
665
666 user = User.get_cached_by_ap_id(data["actor"])
667 assert user.name == "gargle"
668
669 assert user.avatar["url"] == [
670 %{
671 "href" =>
672 "https://cd.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"
673 }
674 ]
675
676 assert user.banner["url"] == [
677 %{
678 "href" =>
679 "https://cd.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
680 }
681 ]
682
683 assert user.bio == "<p>Some bio</p>"
684 end
685
686 test "it works with alsoKnownAs" do
687 {:ok, %Activity{data: %{"actor" => actor}}} =
688 "test/fixtures/mastodon-post-activity.json"
689 |> File.read!()
690 |> Poison.decode!()
691 |> Transmogrifier.handle_incoming()
692
693 assert User.get_cached_by_ap_id(actor).also_known_as == ["http://example.org/users/foo"]
694
695 {:ok, _activity} =
696 "test/fixtures/mastodon-update.json"
697 |> File.read!()
698 |> Poison.decode!()
699 |> Map.put("actor", actor)
700 |> Map.update!("object", fn object ->
701 object
702 |> Map.put("actor", actor)
703 |> Map.put("id", actor)
704 |> Map.put("alsoKnownAs", [
705 "http://mastodon.example.org/users/foo",
706 "http://example.org/users/bar"
707 ])
708 end)
709 |> Transmogrifier.handle_incoming()
710
711 assert User.get_cached_by_ap_id(actor).also_known_as == [
712 "http://mastodon.example.org/users/foo",
713 "http://example.org/users/bar"
714 ]
715 end
716
717 test "it works with custom profile fields" do
718 {:ok, activity} =
719 "test/fixtures/mastodon-post-activity.json"
720 |> File.read!()
721 |> Poison.decode!()
722 |> Transmogrifier.handle_incoming()
723
724 user = User.get_cached_by_ap_id(activity.actor)
725
726 assert User.fields(user) == [
727 %{"name" => "foo", "value" => "bar"},
728 %{"name" => "foo1", "value" => "bar1"}
729 ]
730
731 update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!()
732
733 object =
734 update_data["object"]
735 |> Map.put("actor", user.ap_id)
736 |> Map.put("id", user.ap_id)
737
738 update_data =
739 update_data
740 |> Map.put("actor", user.ap_id)
741 |> Map.put("object", object)
742
743 {:ok, _update_activity} = Transmogrifier.handle_incoming(update_data)
744
745 user = User.get_cached_by_ap_id(user.ap_id)
746
747 assert User.fields(user) == [
748 %{"name" => "foo", "value" => "updated"},
749 %{"name" => "foo1", "value" => "updated"}
750 ]
751
752 Pleroma.Config.put([:instance, :max_remote_account_fields], 2)
753
754 update_data =
755 put_in(update_data, ["object", "attachment"], [
756 %{"name" => "foo", "type" => "PropertyValue", "value" => "bar"},
757 %{"name" => "foo11", "type" => "PropertyValue", "value" => "bar11"},
758 %{"name" => "foo22", "type" => "PropertyValue", "value" => "bar22"}
759 ])
760
761 {:ok, _} = Transmogrifier.handle_incoming(update_data)
762
763 user = User.get_cached_by_ap_id(user.ap_id)
764
765 assert User.fields(user) == [
766 %{"name" => "foo", "value" => "updated"},
767 %{"name" => "foo1", "value" => "updated"}
768 ]
769
770 update_data = put_in(update_data, ["object", "attachment"], [])
771
772 {:ok, _} = Transmogrifier.handle_incoming(update_data)
773
774 user = User.get_cached_by_ap_id(user.ap_id)
775
776 assert User.fields(user) == []
777 end
778
779 test "it works for incoming update activities which lock the account" do
780 data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
781
782 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
783 update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!()
784
785 object =
786 update_data["object"]
787 |> Map.put("actor", data["actor"])
788 |> Map.put("id", data["actor"])
789 |> Map.put("manuallyApprovesFollowers", true)
790
791 update_data =
792 update_data
793 |> Map.put("actor", data["actor"])
794 |> Map.put("object", object)
795
796 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data)
797
798 user = User.get_cached_by_ap_id(data["actor"])
799 assert user.locked == true
800 end
801
802 test "it works for incoming deletes" do
803 activity = insert(:note_activity)
804 deleting_user = insert(:user)
805
806 data =
807 File.read!("test/fixtures/mastodon-delete.json")
808 |> Poison.decode!()
809
810 object =
811 data["object"]
812 |> Map.put("id", activity.data["object"])
813
814 data =
815 data
816 |> Map.put("object", object)
817 |> Map.put("actor", deleting_user.ap_id)
818
819 {:ok, %Activity{actor: actor, local: false, data: %{"id" => id}}} =
820 Transmogrifier.handle_incoming(data)
821
822 assert id == data["id"]
823 refute Activity.get_by_id(activity.id)
824 assert actor == deleting_user.ap_id
825 end
826
827 test "it fails for incoming deletes with spoofed origin" do
828 activity = insert(:note_activity)
829
830 data =
831 File.read!("test/fixtures/mastodon-delete.json")
832 |> Poison.decode!()
833
834 object =
835 data["object"]
836 |> Map.put("id", activity.data["object"])
837
838 data =
839 data
840 |> Map.put("object", object)
841
842 assert capture_log(fn ->
843 :error = Transmogrifier.handle_incoming(data)
844 end) =~
845 "[error] Could not decode user at fetch http://mastodon.example.org/users/gargron, {:error, :nxdomain}"
846
847 assert Activity.get_by_id(activity.id)
848 end
849
850 @tag capture_log: true
851 test "it works for incoming user deletes" do
852 %{ap_id: ap_id} = insert(:user, ap_id: "http://mastodon.example.org/users/admin")
853
854 data =
855 File.read!("test/fixtures/mastodon-delete-user.json")
856 |> Poison.decode!()
857
858 {:ok, _} = Transmogrifier.handle_incoming(data)
859 ObanHelpers.perform_all()
860
861 refute User.get_cached_by_ap_id(ap_id)
862 end
863
864 test "it fails for incoming user deletes with spoofed origin" do
865 %{ap_id: ap_id} = insert(:user)
866
867 data =
868 File.read!("test/fixtures/mastodon-delete-user.json")
869 |> Poison.decode!()
870 |> Map.put("actor", ap_id)
871
872 assert capture_log(fn ->
873 assert :error == Transmogrifier.handle_incoming(data)
874 end) =~ "Object containment failed"
875
876 assert User.get_cached_by_ap_id(ap_id)
877 end
878
879 test "it works for incoming unannounces with an existing notice" do
880 user = insert(:user)
881 {:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
882
883 announce_data =
884 File.read!("test/fixtures/mastodon-announce.json")
885 |> Poison.decode!()
886 |> Map.put("object", activity.data["object"])
887
888 {:ok, %Activity{data: announce_data, local: false}} =
889 Transmogrifier.handle_incoming(announce_data)
890
891 data =
892 File.read!("test/fixtures/mastodon-undo-announce.json")
893 |> Poison.decode!()
894 |> Map.put("object", announce_data)
895 |> Map.put("actor", announce_data["actor"])
896
897 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
898
899 assert data["type"] == "Undo"
900 assert object_data = data["object"]
901 assert object_data["type"] == "Announce"
902 assert object_data["object"] == activity.data["object"]
903
904 assert object_data["id"] ==
905 "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
906 end
907
908 test "it works for incomming unfollows with an existing follow" do
909 user = insert(:user)
910
911 follow_data =
912 File.read!("test/fixtures/mastodon-follow-activity.json")
913 |> Poison.decode!()
914 |> Map.put("object", user.ap_id)
915
916 {:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(follow_data)
917
918 data =
919 File.read!("test/fixtures/mastodon-unfollow-activity.json")
920 |> Poison.decode!()
921 |> Map.put("object", follow_data)
922
923 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
924
925 assert data["type"] == "Undo"
926 assert data["object"]["type"] == "Follow"
927 assert data["object"]["object"] == user.ap_id
928 assert data["actor"] == "http://mastodon.example.org/users/admin"
929
930 refute User.following?(User.get_cached_by_ap_id(data["actor"]), user)
931 end
932
933 test "it works for incoming follows to locked account" do
934 pending_follower = insert(:user, ap_id: "http://mastodon.example.org/users/admin")
935 user = insert(:user, locked: true)
936
937 data =
938 File.read!("test/fixtures/mastodon-follow-activity.json")
939 |> Poison.decode!()
940 |> Map.put("object", user.ap_id)
941
942 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
943
944 assert data["type"] == "Follow"
945 assert data["object"] == user.ap_id
946 assert data["state"] == "pending"
947 assert data["actor"] == "http://mastodon.example.org/users/admin"
948
949 assert [^pending_follower] = User.get_follow_requests(user)
950 end
951
952 test "it works for incoming blocks" do
953 user = insert(:user)
954
955 data =
956 File.read!("test/fixtures/mastodon-block-activity.json")
957 |> Poison.decode!()
958 |> Map.put("object", user.ap_id)
959
960 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
961
962 assert data["type"] == "Block"
963 assert data["object"] == user.ap_id
964 assert data["actor"] == "http://mastodon.example.org/users/admin"
965
966 blocker = User.get_cached_by_ap_id(data["actor"])
967
968 assert User.blocks?(blocker, user)
969 end
970
971 test "incoming blocks successfully tear down any follow relationship" do
972 blocker = insert(:user)
973 blocked = insert(:user)
974
975 data =
976 File.read!("test/fixtures/mastodon-block-activity.json")
977 |> Poison.decode!()
978 |> Map.put("object", blocked.ap_id)
979 |> Map.put("actor", blocker.ap_id)
980
981 {:ok, blocker} = User.follow(blocker, blocked)
982 {:ok, blocked} = User.follow(blocked, blocker)
983
984 assert User.following?(blocker, blocked)
985 assert User.following?(blocked, blocker)
986
987 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
988
989 assert data["type"] == "Block"
990 assert data["object"] == blocked.ap_id
991 assert data["actor"] == blocker.ap_id
992
993 blocker = User.get_cached_by_ap_id(data["actor"])
994 blocked = User.get_cached_by_ap_id(data["object"])
995
996 assert User.blocks?(blocker, blocked)
997
998 refute User.following?(blocker, blocked)
999 refute User.following?(blocked, blocker)
1000 end
1001
1002 test "it works for incoming unblocks with an existing block" do
1003 user = insert(:user)
1004
1005 block_data =
1006 File.read!("test/fixtures/mastodon-block-activity.json")
1007 |> Poison.decode!()
1008 |> Map.put("object", user.ap_id)
1009
1010 {:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(block_data)
1011
1012 data =
1013 File.read!("test/fixtures/mastodon-unblock-activity.json")
1014 |> Poison.decode!()
1015 |> Map.put("object", block_data)
1016
1017 {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
1018 assert data["type"] == "Undo"
1019 assert data["object"]["type"] == "Block"
1020 assert data["object"]["object"] == user.ap_id
1021 assert data["actor"] == "http://mastodon.example.org/users/admin"
1022
1023 blocker = User.get_cached_by_ap_id(data["actor"])
1024
1025 refute User.blocks?(blocker, user)
1026 end
1027
1028 test "it works for incoming accepts which were pre-accepted" do
1029 follower = insert(:user)
1030 followed = insert(:user)
1031
1032 {:ok, follower} = User.follow(follower, followed)
1033 assert User.following?(follower, followed) == true
1034
1035 {:ok, follow_activity} = ActivityPub.follow(follower, followed)
1036
1037 accept_data =
1038 File.read!("test/fixtures/mastodon-accept-activity.json")
1039 |> Poison.decode!()
1040 |> Map.put("actor", followed.ap_id)
1041
1042 object =
1043 accept_data["object"]
1044 |> Map.put("actor", follower.ap_id)
1045 |> Map.put("id", follow_activity.data["id"])
1046
1047 accept_data = Map.put(accept_data, "object", object)
1048
1049 {:ok, activity} = Transmogrifier.handle_incoming(accept_data)
1050 refute activity.local
1051
1052 assert activity.data["object"] == follow_activity.data["id"]
1053
1054 assert activity.data["id"] == accept_data["id"]
1055
1056 follower = User.get_cached_by_id(follower.id)
1057
1058 assert User.following?(follower, followed) == true
1059 end
1060
1061 test "it works for incoming accepts which were orphaned" do
1062 follower = insert(:user)
1063 followed = insert(:user, locked: true)
1064
1065 {:ok, follow_activity} = ActivityPub.follow(follower, followed)
1066
1067 accept_data =
1068 File.read!("test/fixtures/mastodon-accept-activity.json")
1069 |> Poison.decode!()
1070 |> Map.put("actor", followed.ap_id)
1071
1072 accept_data =
1073 Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id))
1074
1075 {:ok, activity} = Transmogrifier.handle_incoming(accept_data)
1076 assert activity.data["object"] == follow_activity.data["id"]
1077
1078 follower = User.get_cached_by_id(follower.id)
1079
1080 assert User.following?(follower, followed) == true
1081 end
1082
1083 test "it works for incoming accepts which are referenced by IRI only" do
1084 follower = insert(:user)
1085 followed = insert(:user, locked: true)
1086
1087 {:ok, follow_activity} = ActivityPub.follow(follower, followed)
1088
1089 accept_data =
1090 File.read!("test/fixtures/mastodon-accept-activity.json")
1091 |> Poison.decode!()
1092 |> Map.put("actor", followed.ap_id)
1093 |> Map.put("object", follow_activity.data["id"])
1094
1095 {:ok, activity} = Transmogrifier.handle_incoming(accept_data)
1096 assert activity.data["object"] == follow_activity.data["id"]
1097
1098 follower = User.get_cached_by_id(follower.id)
1099
1100 assert User.following?(follower, followed) == true
1101 end
1102
1103 test "it fails for incoming accepts which cannot be correlated" do
1104 follower = insert(:user)
1105 followed = insert(:user, locked: true)
1106
1107 accept_data =
1108 File.read!("test/fixtures/mastodon-accept-activity.json")
1109 |> Poison.decode!()
1110 |> Map.put("actor", followed.ap_id)
1111
1112 accept_data =
1113 Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id))
1114
1115 :error = Transmogrifier.handle_incoming(accept_data)
1116
1117 follower = User.get_cached_by_id(follower.id)
1118
1119 refute User.following?(follower, followed) == true
1120 end
1121
1122 test "it fails for incoming rejects which cannot be correlated" do
1123 follower = insert(:user)
1124 followed = insert(:user, locked: true)
1125
1126 accept_data =
1127 File.read!("test/fixtures/mastodon-reject-activity.json")
1128 |> Poison.decode!()
1129 |> Map.put("actor", followed.ap_id)
1130
1131 accept_data =
1132 Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id))
1133
1134 :error = Transmogrifier.handle_incoming(accept_data)
1135
1136 follower = User.get_cached_by_id(follower.id)
1137
1138 refute User.following?(follower, followed) == true
1139 end
1140
1141 test "it works for incoming rejects which are orphaned" do
1142 follower = insert(:user)
1143 followed = insert(:user, locked: true)
1144
1145 {:ok, follower} = User.follow(follower, followed)
1146 {:ok, _follow_activity} = ActivityPub.follow(follower, followed)
1147
1148 assert User.following?(follower, followed) == true
1149
1150 reject_data =
1151 File.read!("test/fixtures/mastodon-reject-activity.json")
1152 |> Poison.decode!()
1153 |> Map.put("actor", followed.ap_id)
1154
1155 reject_data =
1156 Map.put(reject_data, "object", Map.put(reject_data["object"], "actor", follower.ap_id))
1157
1158 {:ok, activity} = Transmogrifier.handle_incoming(reject_data)
1159 refute activity.local
1160 assert activity.data["id"] == reject_data["id"]
1161
1162 follower = User.get_cached_by_id(follower.id)
1163
1164 assert User.following?(follower, followed) == false
1165 end
1166
1167 test "it works for incoming rejects which are referenced by IRI only" do
1168 follower = insert(:user)
1169 followed = insert(:user, locked: true)
1170
1171 {:ok, follower} = User.follow(follower, followed)
1172 {:ok, follow_activity} = ActivityPub.follow(follower, followed)
1173
1174 assert User.following?(follower, followed) == true
1175
1176 reject_data =
1177 File.read!("test/fixtures/mastodon-reject-activity.json")
1178 |> Poison.decode!()
1179 |> Map.put("actor", followed.ap_id)
1180 |> Map.put("object", follow_activity.data["id"])
1181
1182 {:ok, %Activity{data: _}} = Transmogrifier.handle_incoming(reject_data)
1183
1184 follower = User.get_cached_by_id(follower.id)
1185
1186 assert User.following?(follower, followed) == false
1187 end
1188
1189 test "it rejects activities without a valid ID" do
1190 user = insert(:user)
1191
1192 data =
1193 File.read!("test/fixtures/mastodon-follow-activity.json")
1194 |> Poison.decode!()
1195 |> Map.put("object", user.ap_id)
1196 |> Map.put("id", "")
1197
1198 :error = Transmogrifier.handle_incoming(data)
1199 end
1200
1201 test "it remaps video URLs as attachments if necessary" do
1202 {:ok, object} =
1203 Fetcher.fetch_object_from_id(
1204 "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
1205 )
1206
1207 attachment = %{
1208 "type" => "Link",
1209 "mediaType" => "video/mp4",
1210 "href" =>
1211 "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4",
1212 "mimeType" => "video/mp4",
1213 "size" => 5_015_880,
1214 "url" => [
1215 %{
1216 "href" =>
1217 "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4",
1218 "mediaType" => "video/mp4",
1219 "type" => "Link"
1220 }
1221 ],
1222 "width" => 480
1223 }
1224
1225 assert object.data["url"] ==
1226 "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
1227
1228 assert object.data["attachment"] == [attachment]
1229 end
1230
1231 test "it accepts Flag activities" do
1232 user = insert(:user)
1233 other_user = insert(:user)
1234
1235 {:ok, activity} = CommonAPI.post(user, %{"status" => "test post"})
1236 object = Object.normalize(activity)
1237
1238 note_obj = %{
1239 "type" => "Note",
1240 "id" => activity.data["id"],
1241 "content" => "test post",
1242 "published" => object.data["published"],
1243 "actor" => AccountView.render("show.json", %{user: user})
1244 }
1245
1246 message = %{
1247 "@context" => "https://www.w3.org/ns/activitystreams",
1248 "cc" => [user.ap_id],
1249 "object" => [user.ap_id, activity.data["id"]],
1250 "type" => "Flag",
1251 "content" => "blocked AND reported!!!",
1252 "actor" => other_user.ap_id
1253 }
1254
1255 assert {:ok, activity} = Transmogrifier.handle_incoming(message)
1256
1257 assert activity.data["object"] == [user.ap_id, note_obj]
1258 assert activity.data["content"] == "blocked AND reported!!!"
1259 assert activity.data["actor"] == other_user.ap_id
1260 assert activity.data["cc"] == [user.ap_id]
1261 end
1262
1263 test "it correctly processes messages with non-array to field" do
1264 user = insert(:user)
1265
1266 message = %{
1267 "@context" => "https://www.w3.org/ns/activitystreams",
1268 "to" => "https://www.w3.org/ns/activitystreams#Public",
1269 "type" => "Create",
1270 "object" => %{
1271 "content" => "blah blah blah",
1272 "type" => "Note",
1273 "attributedTo" => user.ap_id,
1274 "inReplyTo" => nil
1275 },
1276 "actor" => user.ap_id
1277 }
1278
1279 assert {:ok, activity} = Transmogrifier.handle_incoming(message)
1280
1281 assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["to"]
1282 end
1283
1284 test "it correctly processes messages with non-array cc field" do
1285 user = insert(:user)
1286
1287 message = %{
1288 "@context" => "https://www.w3.org/ns/activitystreams",
1289 "to" => user.follower_address,
1290 "cc" => "https://www.w3.org/ns/activitystreams#Public",
1291 "type" => "Create",
1292 "object" => %{
1293 "content" => "blah blah blah",
1294 "type" => "Note",
1295 "attributedTo" => user.ap_id,
1296 "inReplyTo" => nil
1297 },
1298 "actor" => user.ap_id
1299 }
1300
1301 assert {:ok, activity} = Transmogrifier.handle_incoming(message)
1302
1303 assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["cc"]
1304 assert [user.follower_address] == activity.data["to"]
1305 end
1306
1307 test "it accepts Move activities" do
1308 old_user = insert(:user)
1309 new_user = insert(:user)
1310
1311 message = %{
1312 "@context" => "https://www.w3.org/ns/activitystreams",
1313 "type" => "Move",
1314 "actor" => old_user.ap_id,
1315 "object" => old_user.ap_id,
1316 "target" => new_user.ap_id
1317 }
1318
1319 assert :error = Transmogrifier.handle_incoming(message)
1320
1321 {:ok, _new_user} = User.update_and_set_cache(new_user, %{also_known_as: [old_user.ap_id]})
1322
1323 assert {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(message)
1324 assert activity.actor == old_user.ap_id
1325 assert activity.data["actor"] == old_user.ap_id
1326 assert activity.data["object"] == old_user.ap_id
1327 assert activity.data["target"] == new_user.ap_id
1328 assert activity.data["type"] == "Move"
1329 end
1330 end
1331
1332 describe "prepare outgoing" do
1333 test "it inlines private announced objects" do
1334 user = insert(:user)
1335
1336 {:ok, activity} = CommonAPI.post(user, %{"status" => "hey", "visibility" => "private"})
1337
1338 {:ok, announce_activity, _} = CommonAPI.repeat(activity.id, user)
1339
1340 {:ok, modified} = Transmogrifier.prepare_outgoing(announce_activity.data)
1341
1342 assert modified["object"]["content"] == "hey"
1343 assert modified["object"]["actor"] == modified["object"]["attributedTo"]
1344 end
1345
1346 test "it turns mentions into tags" do
1347 user = insert(:user)
1348 other_user = insert(:user)
1349
1350 {:ok, activity} =
1351 CommonAPI.post(user, %{"status" => "hey, @#{other_user.nickname}, how are ya? #2hu"})
1352
1353 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
1354 object = modified["object"]
1355
1356 expected_mention = %{
1357 "href" => other_user.ap_id,
1358 "name" => "@#{other_user.nickname}",
1359 "type" => "Mention"
1360 }
1361
1362 expected_tag = %{
1363 "href" => Pleroma.Web.Endpoint.url() <> "/tags/2hu",
1364 "type" => "Hashtag",
1365 "name" => "#2hu"
1366 }
1367
1368 assert Enum.member?(object["tag"], expected_tag)
1369 assert Enum.member?(object["tag"], expected_mention)
1370 end
1371
1372 test "it adds the sensitive property" do
1373 user = insert(:user)
1374
1375 {:ok, activity} = CommonAPI.post(user, %{"status" => "#nsfw hey"})
1376 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
1377
1378 assert modified["object"]["sensitive"]
1379 end
1380
1381 test "it adds the json-ld context and the conversation property" do
1382 user = insert(:user)
1383
1384 {:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
1385 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
1386
1387 assert modified["@context"] ==
1388 Pleroma.Web.ActivityPub.Utils.make_json_ld_header()["@context"]
1389
1390 assert modified["object"]["conversation"] == modified["context"]
1391 end
1392
1393 test "it sets the 'attributedTo' property to the actor of the object if it doesn't have one" do
1394 user = insert(:user)
1395
1396 {:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
1397 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
1398
1399 assert modified["object"]["actor"] == modified["object"]["attributedTo"]
1400 end
1401
1402 test "it strips internal hashtag data" do
1403 user = insert(:user)
1404
1405 {:ok, activity} = CommonAPI.post(user, %{"status" => "#2hu"})
1406
1407 expected_tag = %{
1408 "href" => Pleroma.Web.Endpoint.url() <> "/tags/2hu",
1409 "type" => "Hashtag",
1410 "name" => "#2hu"
1411 }
1412
1413 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
1414
1415 assert modified["object"]["tag"] == [expected_tag]
1416 end
1417
1418 test "it strips internal fields" do
1419 user = insert(:user)
1420
1421 {:ok, activity} = CommonAPI.post(user, %{"status" => "#2hu :firefox:"})
1422
1423 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
1424
1425 assert length(modified["object"]["tag"]) == 2
1426
1427 assert is_nil(modified["object"]["emoji"])
1428 assert is_nil(modified["object"]["like_count"])
1429 assert is_nil(modified["object"]["announcements"])
1430 assert is_nil(modified["object"]["announcement_count"])
1431 assert is_nil(modified["object"]["context_id"])
1432 end
1433
1434 test "it strips internal fields of article" do
1435 activity = insert(:article_activity)
1436
1437 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
1438
1439 assert length(modified["object"]["tag"]) == 2
1440
1441 assert is_nil(modified["object"]["emoji"])
1442 assert is_nil(modified["object"]["like_count"])
1443 assert is_nil(modified["object"]["announcements"])
1444 assert is_nil(modified["object"]["announcement_count"])
1445 assert is_nil(modified["object"]["context_id"])
1446 assert is_nil(modified["object"]["likes"])
1447 end
1448
1449 test "the directMessage flag is present" do
1450 user = insert(:user)
1451 other_user = insert(:user)
1452
1453 {:ok, activity} = CommonAPI.post(user, %{"status" => "2hu :moominmamma:"})
1454
1455 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
1456
1457 assert modified["directMessage"] == false
1458
1459 {:ok, activity} =
1460 CommonAPI.post(user, %{"status" => "@#{other_user.nickname} :moominmamma:"})
1461
1462 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
1463
1464 assert modified["directMessage"] == false
1465
1466 {:ok, activity} =
1467 CommonAPI.post(user, %{
1468 "status" => "@#{other_user.nickname} :moominmamma:",
1469 "visibility" => "direct"
1470 })
1471
1472 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
1473
1474 assert modified["directMessage"] == true
1475 end
1476
1477 test "it strips BCC field" do
1478 user = insert(:user)
1479 {:ok, list} = Pleroma.List.create("foo", user)
1480
1481 {:ok, activity} =
1482 CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"})
1483
1484 {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
1485
1486 assert is_nil(modified["bcc"])
1487 end
1488
1489 test "it can handle Listen activities" do
1490 listen_activity = insert(:listen)
1491
1492 {:ok, modified} = Transmogrifier.prepare_outgoing(listen_activity.data)
1493
1494 assert modified["type"] == "Listen"
1495
1496 user = insert(:user)
1497
1498 {:ok, activity} = CommonAPI.listen(user, %{"title" => "lain radio episode 1"})
1499
1500 {:ok, _modified} = Transmogrifier.prepare_outgoing(activity.data)
1501 end
1502 end
1503
1504 describe "user upgrade" do
1505 test "it upgrades a user to activitypub" do
1506 user =
1507 insert(:user, %{
1508 nickname: "rye@niu.moe",
1509 local: false,
1510 ap_id: "https://niu.moe/users/rye",
1511 follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"})
1512 })
1513
1514 user_two = insert(:user)
1515 Pleroma.FollowingRelationship.follow(user_two, user, "accept")
1516
1517 {:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
1518 {:ok, unrelated_activity} = CommonAPI.post(user_two, %{"status" => "test"})
1519 assert "http://localhost:4001/users/rye@niu.moe/followers" in activity.recipients
1520
1521 user = User.get_cached_by_id(user.id)
1522 assert user.note_count == 1
1523
1524 {:ok, user} = Transmogrifier.upgrade_user_from_ap_id("https://niu.moe/users/rye")
1525 ObanHelpers.perform_all()
1526
1527 assert user.ap_enabled
1528 assert user.note_count == 1
1529 assert user.follower_address == "https://niu.moe/users/rye/followers"
1530 assert user.following_address == "https://niu.moe/users/rye/following"
1531
1532 user = User.get_cached_by_id(user.id)
1533 assert user.note_count == 1
1534
1535 activity = Activity.get_by_id(activity.id)
1536 assert user.follower_address in activity.recipients
1537
1538 assert %{
1539 "url" => [
1540 %{
1541 "href" =>
1542 "https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"
1543 }
1544 ]
1545 } = user.avatar
1546
1547 assert %{
1548 "url" => [
1549 %{
1550 "href" =>
1551 "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
1552 }
1553 ]
1554 } = user.banner
1555
1556 refute "..." in activity.recipients
1557
1558 unrelated_activity = Activity.get_by_id(unrelated_activity.id)
1559 refute user.follower_address in unrelated_activity.recipients
1560
1561 user_two = User.get_cached_by_id(user_two.id)
1562 assert User.following?(user_two, user)
1563 refute "..." in User.following(user_two)
1564 end
1565 end
1566
1567 describe "actor rewriting" do
1568 test "it fixes the actor URL property to be a proper URI" do
1569 data = %{
1570 "url" => %{"href" => "http://example.com"}
1571 }
1572
1573 rewritten = Transmogrifier.maybe_fix_user_object(data)
1574 assert rewritten["url"] == "http://example.com"
1575 end
1576 end
1577
1578 describe "actor origin containment" do
1579 test "it rejects activities which reference objects with bogus origins" do
1580 data = %{
1581 "@context" => "https://www.w3.org/ns/activitystreams",
1582 "id" => "http://mastodon.example.org/users/admin/activities/1234",
1583 "actor" => "http://mastodon.example.org/users/admin",
1584 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
1585 "object" => "https://info.pleroma.site/activity.json",
1586 "type" => "Announce"
1587 }
1588
1589 assert capture_log(fn ->
1590 :error = Transmogrifier.handle_incoming(data)
1591 end) =~ "Object containment failed"
1592 end
1593
1594 test "it rejects activities which reference objects that have an incorrect attribution (variant 1)" do
1595 data = %{
1596 "@context" => "https://www.w3.org/ns/activitystreams",
1597 "id" => "http://mastodon.example.org/users/admin/activities/1234",
1598 "actor" => "http://mastodon.example.org/users/admin",
1599 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
1600 "object" => "https://info.pleroma.site/activity2.json",
1601 "type" => "Announce"
1602 }
1603
1604 assert capture_log(fn ->
1605 :error = Transmogrifier.handle_incoming(data)
1606 end) =~ "Object containment failed"
1607 end
1608
1609 test "it rejects activities which reference objects that have an incorrect attribution (variant 2)" do
1610 data = %{
1611 "@context" => "https://www.w3.org/ns/activitystreams",
1612 "id" => "http://mastodon.example.org/users/admin/activities/1234",
1613 "actor" => "http://mastodon.example.org/users/admin",
1614 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
1615 "object" => "https://info.pleroma.site/activity3.json",
1616 "type" => "Announce"
1617 }
1618
1619 assert capture_log(fn ->
1620 :error = Transmogrifier.handle_incoming(data)
1621 end) =~ "Object containment failed"
1622 end
1623 end
1624
1625 describe "reserialization" do
1626 test "successfully reserializes a message with inReplyTo == nil" do
1627 user = insert(:user)
1628
1629 message = %{
1630 "@context" => "https://www.w3.org/ns/activitystreams",
1631 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
1632 "cc" => [],
1633 "type" => "Create",
1634 "object" => %{
1635 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
1636 "cc" => [],
1637 "type" => "Note",
1638 "content" => "Hi",
1639 "inReplyTo" => nil,
1640 "attributedTo" => user.ap_id
1641 },
1642 "actor" => user.ap_id
1643 }
1644
1645 {:ok, activity} = Transmogrifier.handle_incoming(message)
1646
1647 {:ok, _} = Transmogrifier.prepare_outgoing(activity.data)
1648 end
1649
1650 test "successfully reserializes a message with AS2 objects in IR" do
1651 user = insert(:user)
1652
1653 message = %{
1654 "@context" => "https://www.w3.org/ns/activitystreams",
1655 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
1656 "cc" => [],
1657 "type" => "Create",
1658 "object" => %{
1659 "to" => ["https://www.w3.org/ns/activitystreams#Public"],
1660 "cc" => [],
1661 "type" => "Note",
1662 "content" => "Hi",
1663 "inReplyTo" => nil,
1664 "attributedTo" => user.ap_id,
1665 "tag" => [
1666 %{"name" => "#2hu", "href" => "http://example.com/2hu", "type" => "Hashtag"},
1667 %{"name" => "Bob", "href" => "http://example.com/bob", "type" => "Mention"}
1668 ]
1669 },
1670 "actor" => user.ap_id
1671 }
1672
1673 {:ok, activity} = Transmogrifier.handle_incoming(message)
1674
1675 {:ok, _} = Transmogrifier.prepare_outgoing(activity.data)
1676 end
1677 end
1678
1679 test "Rewrites Answers to Notes" do
1680 user = insert(:user)
1681
1682 {:ok, poll_activity} =
1683 CommonAPI.post(user, %{
1684 "status" => "suya...",
1685 "poll" => %{"options" => ["suya", "suya.", "suya.."], "expires_in" => 10}
1686 })
1687
1688 poll_object = Object.normalize(poll_activity)
1689 # TODO: Replace with CommonAPI vote creation when implemented
1690 data =
1691 File.read!("test/fixtures/mastodon-vote.json")
1692 |> Poison.decode!()
1693 |> Kernel.put_in(["to"], user.ap_id)
1694 |> Kernel.put_in(["object", "inReplyTo"], poll_object.data["id"])
1695 |> Kernel.put_in(["object", "to"], user.ap_id)
1696
1697 {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
1698 {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
1699
1700 assert data["object"]["type"] == "Note"
1701 end
1702
1703 describe "fix_explicit_addressing" do
1704 setup do
1705 user = insert(:user)
1706 [user: user]
1707 end
1708
1709 test "moves non-explicitly mentioned actors to cc", %{user: user} do
1710 explicitly_mentioned_actors = [
1711 "https://pleroma.gold/users/user1",
1712 "https://pleroma.gold/user2"
1713 ]
1714
1715 object = %{
1716 "actor" => user.ap_id,
1717 "to" => explicitly_mentioned_actors ++ ["https://social.beepboop.ga/users/dirb"],
1718 "cc" => [],
1719 "tag" =>
1720 Enum.map(explicitly_mentioned_actors, fn href ->
1721 %{"type" => "Mention", "href" => href}
1722 end)
1723 }
1724
1725 fixed_object = Transmogrifier.fix_explicit_addressing(object)
1726 assert Enum.all?(explicitly_mentioned_actors, &(&1 in fixed_object["to"]))
1727 refute "https://social.beepboop.ga/users/dirb" in fixed_object["to"]
1728 assert "https://social.beepboop.ga/users/dirb" in fixed_object["cc"]
1729 end
1730
1731 test "does not move actor's follower collection to cc", %{user: user} do
1732 object = %{
1733 "actor" => user.ap_id,
1734 "to" => [user.follower_address],
1735 "cc" => []
1736 }
1737
1738 fixed_object = Transmogrifier.fix_explicit_addressing(object)
1739 assert user.follower_address in fixed_object["to"]
1740 refute user.follower_address in fixed_object["cc"]
1741 end
1742
1743 test "removes recipient's follower collection from cc", %{user: user} do
1744 recipient = insert(:user)
1745
1746 object = %{
1747 "actor" => user.ap_id,
1748 "to" => [recipient.ap_id, "https://www.w3.org/ns/activitystreams#Public"],
1749 "cc" => [user.follower_address, recipient.follower_address]
1750 }
1751
1752 fixed_object = Transmogrifier.fix_explicit_addressing(object)
1753
1754 assert user.follower_address in fixed_object["cc"]
1755 refute recipient.follower_address in fixed_object["cc"]
1756 refute recipient.follower_address in fixed_object["to"]
1757 end
1758 end
1759
1760 describe "fix_summary/1" do
1761 test "returns fixed object" do
1762 assert Transmogrifier.fix_summary(%{"summary" => nil}) == %{"summary" => ""}
1763 assert Transmogrifier.fix_summary(%{"summary" => "ok"}) == %{"summary" => "ok"}
1764 assert Transmogrifier.fix_summary(%{}) == %{"summary" => ""}
1765 end
1766 end
1767
1768 describe "fix_in_reply_to/2" do
1769 clear_config([:instance, :federation_incoming_replies_max_depth])
1770
1771 setup do
1772 data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
1773 [data: data]
1774 end
1775
1776 test "returns not modified object when hasn't containts inReplyTo field", %{data: data} do
1777 assert Transmogrifier.fix_in_reply_to(data) == data
1778 end
1779
1780 test "returns object with inReplyToAtomUri when denied incoming reply", %{data: data} do
1781 Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0)
1782
1783 object_with_reply =
1784 Map.put(data["object"], "inReplyTo", "https://shitposter.club/notice/2827873")
1785
1786 modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
1787 assert modified_object["inReplyTo"] == "https://shitposter.club/notice/2827873"
1788 assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
1789
1790 object_with_reply =
1791 Map.put(data["object"], "inReplyTo", %{"id" => "https://shitposter.club/notice/2827873"})
1792
1793 modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
1794 assert modified_object["inReplyTo"] == %{"id" => "https://shitposter.club/notice/2827873"}
1795 assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
1796
1797 object_with_reply =
1798 Map.put(data["object"], "inReplyTo", ["https://shitposter.club/notice/2827873"])
1799
1800 modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
1801 assert modified_object["inReplyTo"] == ["https://shitposter.club/notice/2827873"]
1802 assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
1803
1804 object_with_reply = Map.put(data["object"], "inReplyTo", [])
1805 modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
1806 assert modified_object["inReplyTo"] == []
1807 assert modified_object["inReplyToAtomUri"] == ""
1808 end
1809
1810 @tag capture_log: true
1811 test "returns modified object when allowed incoming reply", %{data: data} do
1812 object_with_reply =
1813 Map.put(
1814 data["object"],
1815 "inReplyTo",
1816 "https://shitposter.club/notice/2827873"
1817 )
1818
1819 Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 5)
1820 modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
1821
1822 assert modified_object["inReplyTo"] ==
1823 "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
1824
1825 assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
1826
1827 assert modified_object["conversation"] ==
1828 "tag:shitposter.club,2017-05-05:objectType=thread:nonce=3c16e9c2681f6d26"
1829
1830 assert modified_object["context"] ==
1831 "tag:shitposter.club,2017-05-05:objectType=thread:nonce=3c16e9c2681f6d26"
1832 end
1833 end
1834
1835 describe "fix_url/1" do
1836 test "fixes data for object when url is map" do
1837 object = %{
1838 "url" => %{
1839 "type" => "Link",
1840 "mimeType" => "video/mp4",
1841 "href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
1842 }
1843 }
1844
1845 assert Transmogrifier.fix_url(object) == %{
1846 "url" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
1847 }
1848 end
1849
1850 test "fixes data for video object" do
1851 object = %{
1852 "type" => "Video",
1853 "url" => [
1854 %{
1855 "type" => "Link",
1856 "mimeType" => "video/mp4",
1857 "href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
1858 },
1859 %{
1860 "type" => "Link",
1861 "mimeType" => "video/mp4",
1862 "href" => "https://peertube46fb-ad81-2d4c2d1630e3-240.mp4"
1863 },
1864 %{
1865 "type" => "Link",
1866 "mimeType" => "text/html",
1867 "href" => "https://peertube.-2d4c2d1630e3"
1868 },
1869 %{
1870 "type" => "Link",
1871 "mimeType" => "text/html",
1872 "href" => "https://peertube.-2d4c2d16377-42"
1873 }
1874 ]
1875 }
1876
1877 assert Transmogrifier.fix_url(object) == %{
1878 "attachment" => [
1879 %{
1880 "href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4",
1881 "mimeType" => "video/mp4",
1882 "type" => "Link"
1883 }
1884 ],
1885 "type" => "Video",
1886 "url" => "https://peertube.-2d4c2d1630e3"
1887 }
1888 end
1889
1890 test "fixes url for not Video object" do
1891 object = %{
1892 "type" => "Text",
1893 "url" => [
1894 %{
1895 "type" => "Link",
1896 "mimeType" => "text/html",
1897 "href" => "https://peertube.-2d4c2d1630e3"
1898 },
1899 %{
1900 "type" => "Link",
1901 "mimeType" => "text/html",
1902 "href" => "https://peertube.-2d4c2d16377-42"
1903 }
1904 ]
1905 }
1906
1907 assert Transmogrifier.fix_url(object) == %{
1908 "type" => "Text",
1909 "url" => "https://peertube.-2d4c2d1630e3"
1910 }
1911
1912 assert Transmogrifier.fix_url(%{"type" => "Text", "url" => []}) == %{
1913 "type" => "Text",
1914 "url" => ""
1915 }
1916 end
1917
1918 test "retunrs not modified object" do
1919 assert Transmogrifier.fix_url(%{"type" => "Text"}) == %{"type" => "Text"}
1920 end
1921 end
1922
1923 describe "get_obj_helper/2" do
1924 test "returns nil when cannot normalize object" do
1925 assert capture_log(fn ->
1926 refute Transmogrifier.get_obj_helper("test-obj-id")
1927 end) =~ "Unsupported URI scheme"
1928 end
1929
1930 @tag capture_log: true
1931 test "returns {:ok, %Object{}} for success case" do
1932 assert {:ok, %Object{}} =
1933 Transmogrifier.get_obj_helper("https://shitposter.club/notice/2827873")
1934 end
1935 end
1936
1937 describe "fix_attachments/1" do
1938 test "returns not modified object" do
1939 data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
1940 assert Transmogrifier.fix_attachments(data) == data
1941 end
1942
1943 test "returns modified object when attachment is map" do
1944 assert Transmogrifier.fix_attachments(%{
1945 "attachment" => %{
1946 "mediaType" => "video/mp4",
1947 "url" => "https://peertube.moe/stat-480.mp4"
1948 }
1949 }) == %{
1950 "attachment" => [
1951 %{
1952 "mediaType" => "video/mp4",
1953 "url" => [
1954 %{
1955 "href" => "https://peertube.moe/stat-480.mp4",
1956 "mediaType" => "video/mp4",
1957 "type" => "Link"
1958 }
1959 ]
1960 }
1961 ]
1962 }
1963 end
1964
1965 test "returns modified object when attachment is list" do
1966 assert Transmogrifier.fix_attachments(%{
1967 "attachment" => [
1968 %{"mediaType" => "video/mp4", "url" => "https://pe.er/stat-480.mp4"},
1969 %{"mimeType" => "video/mp4", "href" => "https://pe.er/stat-480.mp4"}
1970 ]
1971 }) == %{
1972 "attachment" => [
1973 %{
1974 "mediaType" => "video/mp4",
1975 "url" => [
1976 %{
1977 "href" => "https://pe.er/stat-480.mp4",
1978 "mediaType" => "video/mp4",
1979 "type" => "Link"
1980 }
1981 ]
1982 },
1983 %{
1984 "href" => "https://pe.er/stat-480.mp4",
1985 "mediaType" => "video/mp4",
1986 "mimeType" => "video/mp4",
1987 "url" => [
1988 %{
1989 "href" => "https://pe.er/stat-480.mp4",
1990 "mediaType" => "video/mp4",
1991 "type" => "Link"
1992 }
1993 ]
1994 }
1995 ]
1996 }
1997 end
1998 end
1999
2000 describe "fix_emoji/1" do
2001 test "returns not modified object when object not contains tags" do
2002 data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
2003 assert Transmogrifier.fix_emoji(data) == data
2004 end
2005
2006 test "returns object with emoji when object contains list tags" do
2007 assert Transmogrifier.fix_emoji(%{
2008 "tag" => [
2009 %{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}},
2010 %{"type" => "Hashtag"}
2011 ]
2012 }) == %{
2013 "emoji" => %{"bib" => "/test"},
2014 "tag" => [
2015 %{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"},
2016 %{"type" => "Hashtag"}
2017 ]
2018 }
2019 end
2020
2021 test "returns object with emoji when object contains map tag" do
2022 assert Transmogrifier.fix_emoji(%{
2023 "tag" => %{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}}
2024 }) == %{
2025 "emoji" => %{"bib" => "/test"},
2026 "tag" => %{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"}
2027 }
2028 end
2029 end
2030 end