diff options
author | rinpatch <rinpatch@sdf.org> | 2019-04-17 12:22:32 +0300 |
---|---|---|
committer | rinpatch <rinpatch@sdf.org> | 2019-04-17 12:22:32 +0300 |
commit | 627e5a0a4992cc19fc65a7e93a09c470c8e2bf33 (patch) | |
tree | 0f38b475e8554863a1cbbd7750c19d4cd1336eb1 /test | |
parent | d6ab701a14f7c9fb4d59953648c425e04725fc62 (diff) | |
parent | 73df3046e014ae16e03f16a9c82921652cefcb54 (diff) | |
download | pleroma-627e5a0a4992cc19fc65a7e93a09c470c8e2bf33.tar.gz |
Merge branch 'develop' into feature/database-compaction
Diffstat (limited to 'test')
136 files changed, 18409 insertions, 1915 deletions
diff --git a/test/activity_test.exs b/test/activity_test.exs index 55849c522..dc9c56a21 100644 --- a/test/activity_test.exs +++ b/test/activity_test.exs @@ -1,17 +1,22 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.ActivityTest do use Pleroma.DataCase + alias Pleroma.Activity import Pleroma.Factory test "returns an activity by it's AP id" do activity = insert(:note_activity) - found_activity = Pleroma.Activity.get_by_ap_id(activity.data["id"]) + found_activity = Activity.get_by_ap_id(activity.data["id"]) assert activity == found_activity end test "returns activities by it's objects AP ids" do activity = insert(:note_activity) - [found_activity] = Pleroma.Activity.all_by_object_ap_id(activity.data["object"]["id"]) + [found_activity] = Activity.get_all_create_by_object_ap_id(activity.data["object"]["id"]) assert activity == found_activity end @@ -19,9 +24,22 @@ defmodule Pleroma.ActivityTest do test "returns the activity that created an object" do activity = insert(:note_activity) - found_activity = - Pleroma.Activity.get_create_activity_by_object_ap_id(activity.data["object"]["id"]) + found_activity = Activity.get_create_by_object_ap_id(activity.data["object"]["id"]) assert activity == found_activity end + + test "reply count" do + %{id: id, data: %{"object" => %{"id" => object_ap_id}}} = activity = insert(:note_activity) + + replies_count = activity.data["object"]["repliesCount"] || 0 + expected_increase = replies_count + 1 + Activity.increase_replies_count(object_ap_id) + %{data: %{"object" => %{"repliesCount" => actual_increase}}} = Activity.get_by_id(id) + assert expected_increase == actual_increase + expected_decrease = expected_increase - 1 + Activity.decrease_replies_count(object_ap_id) + %{data: %{"object" => %{"repliesCount" => actual_decrease}}} = Activity.get_by_id(id) + assert expected_decrease == actual_decrease + end end diff --git a/test/captcha_test.exs b/test/captcha_test.exs new file mode 100644 index 000000000..7ca9a4607 --- /dev/null +++ b/test/captcha_test.exs @@ -0,0 +1,46 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.CaptchaTest do + use ExUnit.Case + + import Tesla.Mock + + alias Pleroma.Captcha.Kocaptcha + + @ets_options [:ordered_set, :private, :named_table, {:read_concurrency, true}] + + describe "Kocaptcha" do + setup do + ets_name = Kocaptcha.Ets + ^ets_name = :ets.new(ets_name, @ets_options) + + mock(fn + %{method: :get, url: "https://captcha.kotobank.ch/new"} -> + json(%{ + md5: "63615261b77f5354fb8c4e4986477555", + token: "afa1815e14e29355e6c8f6b143a39fa2", + url: "/captchas/afa1815e14e29355e6c8f6b143a39fa2.png" + }) + end) + + :ok + end + + test "new and validate" do + new = Kocaptcha.new() + assert new[:type] == :kocaptcha + assert new[:token] == "afa1815e14e29355e6c8f6b143a39fa2" + + assert new[:url] == + "https://captcha.kotobank.ch/captchas/afa1815e14e29355e6c8f6b143a39fa2.png" + + assert Kocaptcha.validate( + new[:token], + "7oEy8c", + new[:answer_data] + ) == :ok + end + end +end diff --git a/test/config_test.exs b/test/config_test.exs index 837cbb30c..0a6f0395a 100644 --- a/test/config_test.exs +++ b/test/config_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.ConfigTest do use ExUnit.Case diff --git a/test/emoji_test.exs b/test/emoji_test.exs new file mode 100644 index 000000000..cb1d62d00 --- /dev/null +++ b/test/emoji_test.exs @@ -0,0 +1,106 @@ +defmodule Pleroma.EmojiTest do + use ExUnit.Case, async: true + alias Pleroma.Emoji + + describe "get_all/0" do + setup do + emoji_list = Emoji.get_all() + {:ok, emoji_list: emoji_list} + end + + test "first emoji", %{emoji_list: emoji_list} do + [emoji | _others] = emoji_list + {code, path, tags} = emoji + + assert tuple_size(emoji) == 3 + assert is_binary(code) + assert is_binary(path) + assert is_binary(tags) + end + + test "random emoji", %{emoji_list: emoji_list} do + emoji = Enum.random(emoji_list) + {code, path, tags} = emoji + + assert tuple_size(emoji) == 3 + assert is_binary(code) + assert is_binary(path) + assert is_binary(tags) + end + end + + describe "match_extra/2" do + setup do + groups = [ + "list of files": ["/emoji/custom/first_file.png", "/emoji/custom/second_file.png"], + "wildcard folder": "/emoji/custom/*/file.png", + "wildcard files": "/emoji/custom/folder/*.png", + "special file": "/emoji/custom/special.png" + ] + + {:ok, groups: groups} + end + + test "config for list of files", %{groups: groups} do + group = + groups + |> Emoji.match_extra("/emoji/custom/first_file.png") + |> to_string() + + assert group == "list of files" + end + + test "config with wildcard folder", %{groups: groups} do + group = + groups + |> Emoji.match_extra("/emoji/custom/some_folder/file.png") + |> to_string() + + assert group == "wildcard folder" + end + + test "config with wildcard folder and subfolders", %{groups: groups} do + group = + groups + |> Emoji.match_extra("/emoji/custom/some_folder/another_folder/file.png") + |> to_string() + + assert group == "wildcard folder" + end + + test "config with wildcard files", %{groups: groups} do + group = + groups + |> Emoji.match_extra("/emoji/custom/folder/some_file.png") + |> to_string() + + assert group == "wildcard files" + end + + test "config with wildcard files and subfolders", %{groups: groups} do + group = + groups + |> Emoji.match_extra("/emoji/custom/folder/another_folder/some_file.png") + |> to_string() + + assert group == "wildcard files" + end + + test "config for special file", %{groups: groups} do + group = + groups + |> Emoji.match_extra("/emoji/custom/special.png") + |> to_string() + + assert group == "special file" + end + + test "no mathing returns nil", %{groups: groups} do + group = + groups + |> Emoji.match_extra("/emoji/some_undefined.png") + + refute group + end + end +end diff --git a/test/filter_test.exs b/test/filter_test.exs index 509c15317..b31c68efd 100644 --- a/test/filter_test.exs +++ b/test/filter_test.exs @@ -1,9 +1,12 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.FilterTest do - alias Pleroma.{User, Repo} + alias Pleroma.Repo use Pleroma.DataCase import Pleroma.Factory - import Ecto.Query describe "creating filters" do test "creating one filter" do @@ -99,7 +102,7 @@ defmodule Pleroma.FilterTest do context: ["home"] } - {:ok, filter} = Pleroma.Filter.create(query) + {:ok, _filter} = Pleroma.Filter.create(query) {:ok, filter} = Pleroma.Filter.delete(query) assert is_nil(Repo.get(Pleroma.Filter, filter.filter_id)) end diff --git a/test/fixtures/activitypub-client-post-activity.json b/test/fixtures/activitypub-client-post-activity.json new file mode 100644 index 000000000..c985e072b --- /dev/null +++ b/test/fixtures/activitypub-client-post-activity.json @@ -0,0 +1,9 @@ +{ + "@context": ["https://www.w3.org/ns/activitystreams", {"@language": "en-GB"}], + "type": "Create", + "object": { + "type": "Note", + "content": "It's a note" + }, + "to": ["https://www.w3.org/ns/activitystreams#Public"] +} diff --git a/test/fixtures/httpoison_mock/emelie.atom b/test/fixtures/httpoison_mock/emelie.atom new file mode 100644 index 000000000..ddaa1c6ca --- /dev/null +++ b/test/fixtures/httpoison_mock/emelie.atom @@ -0,0 +1,306 @@ +<?xml version="1.0"?> +<feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:media="http://purl.org/syndication/atommedia" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:mastodon="http://mastodon.social/schema/1.0"> + <id>https://mastodon.social/users/emelie.atom</id> + <title>emelie 🎨</title> + <subtitle>23 / #Sweden / #Artist / #Equestrian / #GameDev + +If I ain't spending time with my pets, I'm probably drawing. 🐴 🐱 🐰</subtitle> + <updated>2019-02-04T20:22:19Z</updated> + <logo>https://files.mastodon.social/accounts/avatars/000/015/657/original/e7163f98280da1a4.png</logo> + <author> + <id>https://mastodon.social/users/emelie</id> + <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type> + <uri>https://mastodon.social/users/emelie</uri> + <name>emelie</name> + <email>emelie@mastodon.social</email> + <summary type="html"><p>23 / <a href="https://mastodon.social/tags/sweden" class="mention hashtag" rel="tag">#<span>Sweden</span></a> / <a href="https://mastodon.social/tags/artist" class="mention hashtag" rel="tag">#<span>Artist</span></a> / <a href="https://mastodon.social/tags/equestrian" class="mention hashtag" rel="tag">#<span>Equestrian</span></a> / <a href="https://mastodon.social/tags/gamedev" class="mention hashtag" rel="tag">#<span>GameDev</span></a></p><p>If I ain&apos;t spending time with my pets, I&apos;m probably drawing. 🐴 🐱 🐰</p></summary> + <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie"/> + <link rel="avatar" type="image/png" media:width="120" media:height="120" href="https://files.mastodon.social/accounts/avatars/000/015/657/original/e7163f98280da1a4.png"/> + <link rel="header" type="image/png" media:width="700" media:height="335" href="https://files.mastodon.social/accounts/headers/000/015/657/original/847f331f3dd9e38b.png"/> + <poco:preferredUsername>emelie</poco:preferredUsername> + <poco:displayName>emelie 🎨</poco:displayName> + <poco:note>23 / #Sweden / #Artist / #Equestrian / #GameDev + +If I ain't spending time with my pets, I'm probably drawing. 🐴 🐱 🐰</poco:note> + <mastodon:scope>public</mastodon:scope> + </author> + <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie"/> + <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie.atom"/> + <link rel="hub" href="https://mastodon.social/api/push"/> + <link rel="salmon" href="https://mastodon.social/api/salmon/15657"/> + <entry> + <id>https://mastodon.social/users/emelie/statuses/101850331907006641</id> + <published>2019-04-01T09:58:50Z</published> + <updated>2019-04-01T09:58:50Z</updated> + <title>New status by emelie</title> + <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> + <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> + <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101850331907006641"/> + <content type="html" xml:lang="en"><p>Me: I&apos;m going to make this vital change to my world building in the morning, no way I&apos;ll forget this, it&apos;s too big of a deal<br />Also me: forgets</p></content> + <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> + <mastodon:scope>public</mastodon:scope> + <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101850331907006641"/> + <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17854598.atom"/> + <ostatus:conversation ref="tag:mastodon.social,2019-04-01:objectId=94383214:objectType=Conversation"/> + </entry> + <entry> + <id>https://mastodon.social/users/emelie/statuses/101849626603073336</id> + <published>2019-04-01T06:59:28Z</published> + <updated>2019-04-01T06:59:28Z</updated> + <title>New status by emelie</title> + <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type> + <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> + <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101849626603073336"/> + <content type="html" xml:lang="sv"><p><span class="h-card"><a href="https://mastodon.social/@Fergant" class="u-url mention">@<span>Fergant</span></a></span> Dom är i stort sett religiös skrift vid det här laget 👏👏</p><p>har dock bara läst svenska översättningen, kanske är dags att jag läser dom på engelska</p></content> + <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://mastodon.social/users/Fergant"/> + <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> + <mastodon:scope>public</mastodon:scope> + <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101849626603073336"/> + <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17852590.atom"/> + <thr:in-reply-to ref="https://mastodon.social/users/Fergant/statuses/101849606513357387" href="https://mastodon.social/@Fergant/101849606513357387"/> + <ostatus:conversation ref="tag:mastodon.social,2019-04-01:objectId=94362529:objectType=Conversation"/> + </entry> + <entry> + <id>https://mastodon.social/users/emelie/statuses/101849580030237068</id> + <published>2019-04-01T06:47:37Z</published> + <updated>2019-04-01T06:47:37Z</updated> + <title>New status by emelie</title> + <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> + <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> + <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101849580030237068"/> + <content type="html" xml:lang="en"><p>What&apos;s you people&apos;s favourite fantasy books? Give me some hot tips 🌞</p></content> + <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> + <mastodon:scope>public</mastodon:scope> + <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101849580030237068"/> + <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17852464.atom"/> + <ostatus:conversation ref="tag:mastodon.social,2019-04-01:objectId=94362529:objectType=Conversation"/> + </entry> + <entry> + <id>https://mastodon.social/users/emelie/statuses/101849550599949363</id> + <published>2019-04-01T06:40:08Z</published> + <updated>2019-04-01T06:40:08Z</updated> + <title>New status by emelie</title> + <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> + <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> + <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101849550599949363"/> + <content type="html" xml:lang="en"><p>Stick them legs out 💃 <a href="https://mastodon.social/tags/mastocats" class="mention hashtag" rel="tag">#<span>mastocats</span></a></p></content> + <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> + <category term="mastocats"/> + <link rel="enclosure" type="image/jpeg" length="516384" href="https://files.mastodon.social/media_attachments/files/013/051/707/original/125a310abe9a34aa.jpeg"/> + <mastodon:scope>public</mastodon:scope> + <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101849550599949363"/> + <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17852407.atom"/> + <ostatus:conversation ref="tag:mastodon.social,2019-04-01:objectId=94361580:objectType=Conversation"/> + </entry> + <entry> + <id>https://mastodon.social/users/emelie/statuses/101849191533152720</id> + <published>2019-04-01T05:08:49Z</published> + <updated>2019-04-01T05:08:49Z</updated> + <title>New status by emelie</title> + <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> + <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> + <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101849191533152720"/> + <content type="html" xml:lang="en"><p>long 🐱 <a href="https://mastodon.social/tags/mastocats" class="mention hashtag" rel="tag">#<span>mastocats</span></a></p></content> + <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> + <category term="mastocats"/> + <link rel="enclosure" type="image/jpeg" length="305208" href="https://files.mastodon.social/media_attachments/files/013/049/940/original/f2dbbfe7de3a17d2.jpeg"/> + <mastodon:scope>public</mastodon:scope> + <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101849191533152720"/> + <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17851663.atom"/> + <ostatus:conversation ref="tag:mastodon.social,2019-04-01:objectId=94351141:objectType=Conversation"/> + </entry> + <entry> + <id>https://mastodon.social/users/emelie/statuses/101849165031453009</id> + <published>2019-04-01T05:02:05Z</published> + <updated>2019-04-01T05:02:05Z</updated> + <title>New status by emelie</title> + <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> + <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> + <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101849165031453009"/> + <content type="html" xml:lang="en"><p>You gotta take whatever bellyrubbing opportunity you can get before she changes her mind 🦁 <a href="https://mastodon.social/tags/mastocats" class="mention hashtag" rel="tag">#<span>mastocats</span></a></p></content> + <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> + <category term="mastocats"/> + <link rel="enclosure" type="video/mp4" length="9838915" href="https://files.mastodon.social/media_attachments/files/013/049/816/original/e7831178a5e0d6d4.mp4"/> + <mastodon:scope>public</mastodon:scope> + <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101849165031453009"/> + <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17851558.atom"/> + <ostatus:conversation ref="tag:mastodon.social,2019-04-01:objectId=94350309:objectType=Conversation"/> + </entry> + <entry> + <id>https://mastodon.social/users/emelie/statuses/101846512530748693</id> + <published>2019-03-31T17:47:31Z</published> + <updated>2019-03-31T17:47:31Z</updated> + <title>New status by emelie</title> + <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> + <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> + <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101846512530748693"/> + <content type="html" xml:lang="en"><p>Hello look at this boy having a decent haircut for once <a href="https://mastodon.social/tags/mastohorses" class="mention hashtag" rel="tag">#<span>mastohorses</span></a> <a href="https://mastodon.social/tags/equestrian" class="mention hashtag" rel="tag">#<span>equestrian</span></a></p></content> + <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> + <category term="equestrian"/> + <category term="mastohorses"/> + <link rel="enclosure" type="image/jpeg" length="461632" href="https://files.mastodon.social/media_attachments/files/013/033/387/original/301e8ab668cd61d2.jpeg"/> + <mastodon:scope>public</mastodon:scope> + <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101846512530748693"/> + <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17842424.atom"/> + <ostatus:conversation ref="tag:mastodon.social,2019-03-31:objectId=94256415:objectType=Conversation"/> + </entry> + <entry> + <id>https://mastodon.social/users/emelie/statuses/101846181093805500</id> + <published>2019-03-31T16:23:14Z</published> + <updated>2019-03-31T16:23:14Z</updated> + <title>New status by emelie</title> + <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> + <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> + <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101846181093805500"/> + <content type="html" xml:lang="en"><p>Sorry did I disturb the who-is-the-longest-cat competition ? <a href="https://mastodon.social/tags/mastocats" class="mention hashtag" rel="tag">#<span>mastocats</span></a></p></content> + <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> + <category term="mastocats"/> + <link rel="enclosure" type="image/jpeg" length="211384" href="https://files.mastodon.social/media_attachments/files/013/030/725/original/5b4886730cbbd25c.jpeg"/> + <mastodon:scope>public</mastodon:scope> + <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101846181093805500"/> + <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17841108.atom"/> + <ostatus:conversation ref="tag:mastodon.social,2019-03-31:objectId=94245239:objectType=Conversation"/> + </entry> + <entry> + <id>https://mastodon.social/users/emelie/statuses/101845897513133849</id> + <published>2019-03-31T15:11:07Z</published> + <updated>2019-03-31T15:11:07Z</updated> + <title>New status by emelie</title> + <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> + <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> + <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101845897513133849"/> + <summary xml:lang="en">more earthsea ramblings</summary> + <content type="html" xml:lang="en"><p>I&apos;m re-watching Tales from Earthsea for the first time since I read the books, and that Therru doesn&apos;t squash Cob like a spider, as Orm Embar did is a wasted opportunity tbh</p></content> + <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> + <mastodon:scope>public</mastodon:scope> + <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101845897513133849"/> + <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17840088.atom"/> + <ostatus:conversation ref="tag:mastodon.social,2019-03-31:objectId=94232455:objectType=Conversation"/> + </entry> + <entry> + <id>https://mastodon.social/users/emelie/statuses/101841219051533307</id> + <published>2019-03-30T19:21:19Z</published> + <updated>2019-03-30T19:21:19Z</updated> + <title>New status by emelie</title> + <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> + <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> + <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101841219051533307"/> + <content type="html" xml:lang="en"><p>I gave my cats some mackerel and they ate it all in 0.3 seconds, and now they won&apos;t stop meowing for more, and I&apos;m tired plz shut up</p></content> + <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> + <mastodon:scope>public</mastodon:scope> + <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101841219051533307"/> + <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17826587.atom"/> + <ostatus:conversation ref="tag:mastodon.social,2019-03-30:objectId=94075000:objectType=Conversation"/> + </entry> + <entry> + <id>https://mastodon.social/users/emelie/statuses/101839949762341381</id> + <published>2019-03-30T13:58:31Z</published> + <updated>2019-03-30T13:58:31Z</updated> + <title>New status by emelie</title> + <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type> + <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> + <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101839949762341381"/> + <content type="html" xml:lang="en"><p>yet I&apos;m confused about this american dude with a gun, like the heck r ya doin in mah ghibli</p></content> + <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> + <mastodon:scope>public</mastodon:scope> + <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101839949762341381"/> + <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17821757.atom"/> + <thr:in-reply-to ref="https://mastodon.social/users/emelie/statuses/101839928677863590" href="https://mastodon.social/@emelie/101839928677863590"/> + <ostatus:conversation ref="tag:mastodon.social,2019-03-30:objectId=94026360:objectType=Conversation"/> + </entry> + <entry> + <id>https://mastodon.social/users/emelie/statuses/101839928677863590</id> + <published>2019-03-30T13:53:09Z</published> + <updated>2019-03-30T13:53:09Z</updated> + <title>New status by emelie</title> + <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> + <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> + <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101839928677863590"/> + <content type="html" xml:lang="en"><p>2 hours into Ni no Kuni 2 and I&apos;ve already sold my soul to this game</p></content> + <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> + <mastodon:scope>public</mastodon:scope> + <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101839928677863590"/> + <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17821713.atom"/> + <ostatus:conversation ref="tag:mastodon.social,2019-03-30:objectId=94026360:objectType=Conversation"/> + </entry> + <entry> + <id>https://mastodon.social/users/emelie/statuses/101836329521599438</id> + <published>2019-03-29T22:37:51Z</published> + <updated>2019-03-29T22:37:51Z</updated> + <title>New status by emelie</title> + <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> + <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> + <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101836329521599438"/> + <content type="html" xml:lang="en"><p>Pippi Longstocking the original one-punch /man</p></content> + <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> + <mastodon:scope>public</mastodon:scope> + <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101836329521599438"/> + <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17811608.atom"/> + <ostatus:conversation ref="tag:mastodon.social,2019-03-29:objectId=93907854:objectType=Conversation"/> + </entry> + <entry> + <id>https://mastodon.social/users/emelie/statuses/101835905282948341</id> + <published>2019-03-29T20:49:57Z</published> + <updated>2019-03-29T20:49:57Z</updated> + <title>New status by emelie</title> + <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> + <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> + <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101835905282948341"/> + <content type="html" xml:lang="en"><p>I&apos;ve had so much wine I thought I had a 3rd brother</p></content> + <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> + <mastodon:scope>public</mastodon:scope> + <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101835905282948341"/> + <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17809862.atom"/> + <ostatus:conversation ref="tag:mastodon.social,2019-03-29:objectId=93892966:objectType=Conversation"/> + </entry> + <entry> + <id>https://mastodon.social/users/emelie/statuses/101835878059204660</id> + <published>2019-03-29T20:43:02Z</published> + <updated>2019-03-29T20:43:02Z</updated> + <title>New status by emelie</title> + <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> + <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> + <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101835878059204660"/> + <content type="html" xml:lang="en"><p>ååååhhh booi</p></content> + <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> + <mastodon:scope>public</mastodon:scope> + <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101835878059204660"/> + <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17809734.atom"/> + <ostatus:conversation ref="tag:mastodon.social,2019-03-29:objectId=93892010:objectType=Conversation"/> + </entry> + <entry> + <id>https://mastodon.social/users/emelie/statuses/101835848050598939</id> + <published>2019-03-29T20:35:24Z</published> + <updated>2019-03-29T20:35:24Z</updated> + <title>New status by emelie</title> + <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type> + <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> + <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101835848050598939"/> + <content type="html" xml:lang="en"><p><span class="h-card"><a href="https://thraeryn.net/@thraeryn" class="u-url mention">@<span>thraeryn</span></a></span> if I spent 1 hour and a half watching this monstrosity, I need to</p></content> + <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://thraeryn.net/users/thraeryn"/> + <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> + <mastodon:scope>public</mastodon:scope> + <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101835848050598939"/> + <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17809591.atom"/> + <thr:in-reply-to ref="https://thraeryn.net/users/thraeryn/statuses/101835839202826007" href="https://thraeryn.net/@thraeryn/101835839202826007"/> + <ostatus:conversation ref="tag:mastodon.social,2019-03-29:objectId=93888827:objectType=Conversation"/> + </entry> + <entry> + <id>https://mastodon.social/users/emelie/statuses/101835823138262290</id> + <published>2019-03-29T20:29:04Z</published> + <updated>2019-03-29T20:29:04Z</updated> + <title>New status by emelie</title> + <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type> + <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb> + <link rel="alternate" type="application/activity+json" href="https://mastodon.social/users/emelie/statuses/101835823138262290"/> + <summary xml:lang="en">medical, fluids mention</summary> + <content type="html" xml:lang="en"><p><span class="h-card"><a href="https://icosahedron.website/@Trev" class="u-url mention">@<span>Trev</span></a></span> *hugs* ✨</p></content> + <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://icosahedron.website/users/Trev"/> + <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> + <mastodon:scope>public</mastodon:scope> + <link rel="alternate" type="text/html" href="https://mastodon.social/@emelie/101835823138262290"/> + <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/emelie/updates/17809468.atom"/> + <thr:in-reply-to ref="https://icosahedron.website/users/Trev/statuses/101835812250051801" href="https://icosahedron.website/@Trev/101835812250051801"/> + <ostatus:conversation ref="tag:icosahedron.website,2019-03-29:objectId=12220882:objectType=Conversation"/> + </entry> +</feed> diff --git a/test/fixtures/httpoison_mock/framatube.org_host_meta b/test/fixtures/httpoison_mock/framatube.org_host_meta new file mode 100644 index 000000000..91516ff6d --- /dev/null +++ b/test/fixtures/httpoison_mock/framatube.org_host_meta @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><hm:Host xmlns:hm="http://host-meta.net/xrd/1.0">framatube.org</hm:Host><Link rel="lrdd" template="http://framatube.org/main/xrd?uri={uri}"><Title>Resource Descriptor</Title></Link></XRD> diff --git a/test/fixtures/httpoison_mock/gerzilla.de_host_meta b/test/fixtures/httpoison_mock/gerzilla.de_host_meta new file mode 100644 index 000000000..fae8f37eb --- /dev/null +++ b/test/fixtures/httpoison_mock/gerzilla.de_host_meta @@ -0,0 +1,10 @@ +<?xml version='1.0' encoding='UTF-8'?><XRD xmlns='http://docs.oasis-open.org/ns/xri/xrd-1.0' + xmlns:hm='http://host-meta.net/xrd/1.0'> + + <hm:Host>gerzilla.de</hm:Host> + + <Link rel='lrdd' type="application/xrd+xml" template='https://gerzilla.de/xrd/?uri={uri}' /> + <Link rel="http://oexchange.org/spec/0.8/rel/resident-target" type="application/xrd+xml" + href="https://gerzilla.de/oexchange/xrd" /> + +</XRD> diff --git a/test/fixtures/httpoison_mock/gnusocial.de_host_meta b/test/fixtures/httpoison_mock/gnusocial.de_host_meta new file mode 100644 index 000000000..a4affb102 --- /dev/null +++ b/test/fixtures/httpoison_mock/gnusocial.de_host_meta @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><hm:Host xmlns:hm="http://host-meta.net/xrd/1.0">gnusocial.de</hm:Host><Link rel="lrdd" template="http://gnusocial.de/main/xrd?uri={uri}"><Title>Resource Descriptor</Title></Link></XRD> diff --git a/test/fixtures/httpoison_mock/status.emelie.json b/test/fixtures/httpoison_mock/status.emelie.json new file mode 100644 index 000000000..4aada0377 --- /dev/null +++ b/test/fixtures/httpoison_mock/status.emelie.json @@ -0,0 +1,64 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + { + "ostatus": "http://ostatus.org#", + "atomUri": "ostatus:atomUri", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "sensitive": "as:sensitive", + "Hashtag": "as:Hashtag", + "toot": "http://joinmastodon.org/ns#", + "Emoji": "toot:Emoji", + "focalPoint": { + "@container": "@list", + "@id": "toot:focalPoint" + } + } + ], + "id": "https://mastodon.social/users/emelie/statuses/101849165031453009", + "type": "Note", + "summary": null, + "inReplyTo": null, + "published": "2019-04-01T05:02:05Z", + "url": "https://mastodon.social/@emelie/101849165031453009", + "attributedTo": "https://mastodon.social/users/emelie", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "https://mastodon.social/users/emelie/followers" + ], + "sensitive": false, + "atomUri": "https://mastodon.social/users/emelie/statuses/101849165031453009", + "inReplyToAtomUri": null, + "conversation": "tag:mastodon.social,2019-04-01:objectId=94350309:objectType=Conversation", + "content": "<p>You gotta take whatever bellyrubbing opportunity you can get before she changes her mind 🦁 <a href=\"https://mastodon.social/tags/mastocats\" class=\"mention hashtag\" rel=\"tag\">#<span>mastocats</span></a></p>", + "contentMap": { + "en": "<p>You gotta take whatever bellyrubbing opportunity you can get before she changes her mind 🦁 <a href=\"https://mastodon.social/tags/mastocats\" class=\"mention hashtag\" rel=\"tag\">#<span>mastocats</span></a></p>" + }, + "attachment": [ + { + "type": "Document", + "mediaType": "video/mp4", + "url": "https://files.mastodon.social/media_attachments/files/013/049/816/original/e7831178a5e0d6d4.mp4", + "name": null + } + ], + "tag": [ + { + "type": "Hashtag", + "href": "https://mastodon.social/tags/mastocats", + "name": "#mastocats" + } + ], + "replies": { + "id": "https://mastodon.social/users/emelie/statuses/101849165031453009/replies", + "type": "Collection", + "first": { + "type": "CollectionPage", + "partOf": "https://mastodon.social/users/emelie/statuses/101849165031453009/replies", + "items": [] + } + } +} diff --git a/test/fixtures/httpoison_mock/webfinger_emelie.json b/test/fixtures/httpoison_mock/webfinger_emelie.json new file mode 100644 index 000000000..0b61cb618 --- /dev/null +++ b/test/fixtures/httpoison_mock/webfinger_emelie.json @@ -0,0 +1,36 @@ +{ + "aliases": [ + "https://mastodon.social/@emelie", + "https://mastodon.social/users/emelie" + ], + "links": [ + { + "href": "https://mastodon.social/@emelie", + "rel": "http://webfinger.net/rel/profile-page", + "type": "text/html" + }, + { + "href": "https://mastodon.social/users/emelie.atom", + "rel": "http://schemas.google.com/g/2010#updates-from", + "type": "application/atom+xml" + }, + { + "href": "https://mastodon.social/users/emelie", + "rel": "self", + "type": "application/activity+json" + }, + { + "href": "https://mastodon.social/api/salmon/15657", + "rel": "salmon" + }, + { + "href": "data:application/magic-public-key,RSA.u3CWs1oAJPE3ZJ9sj6Ut_Mu-mTE7MOijsQc8_6c73XVVuhIEomiozJIH7l8a7S1n5SYL4UuiwcubSOi7u1bbGpYnp5TYhN-Cxvq_P80V4_ncNIPSQzS49it7nSLeG5pA21lGPDA44huquES1un6p9gSmbTwngVX9oe4MYuUeh0Z7vijjU13Llz1cRq_ZgPQPgfz-2NJf-VeXnvyDZDYxZPVBBlrMl3VoGbu0M5L8SjY35559KCZ3woIvqRolcoHXfgvJMdPcJgSZVYxlCw3dA95q9jQcn6s87CPSUs7bmYEQCrDVn5m5NER5TzwBmP4cgJl9AaDVWQtRd4jFZNTxlQ==.AQAB", + "rel": "magic-public-key" + }, + { + "rel": "http://ostatus.org/schema/1.0/subscribe", + "template": "https://mastodon.social/authorize_interaction?uri={uri}" + } + ], + "subject": "acct:emelie@mastodon.social" +} diff --git a/test/fixtures/image.jpg b/test/fixtures/image.jpg Binary files differindex 09834bb5c..edff6246b 100644 --- a/test/fixtures/image.jpg +++ b/test/fixtures/image.jpg diff --git a/test/fixtures/lambadalambda.json b/test/fixtures/lambadalambda.json new file mode 100644 index 000000000..1f09fb591 --- /dev/null +++ b/test/fixtures/lambadalambda.json @@ -0,0 +1,64 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "toot": "http://joinmastodon.org/ns#", + "featured": { + "@id": "toot:featured", + "@type": "@id" + }, + "alsoKnownAs": { + "@id": "as:alsoKnownAs", + "@type": "@id" + }, + "movedTo": { + "@id": "as:movedTo", + "@type": "@id" + }, + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value", + "Hashtag": "as:Hashtag", + "Emoji": "toot:Emoji", + "IdentityProof": "toot:IdentityProof", + "focalPoint": { + "@container": "@list", + "@id": "toot:focalPoint" + } + } + ], + "id": "https://mastodon.social/users/lambadalambda", + "type": "Person", + "following": "https://mastodon.social/users/lambadalambda/following", + "followers": "https://mastodon.social/users/lambadalambda/followers", + "inbox": "https://mastodon.social/users/lambadalambda/inbox", + "outbox": "https://mastodon.social/users/lambadalambda/outbox", + "featured": "https://mastodon.social/users/lambadalambda/collections/featured", + "preferredUsername": "lambadalambda", + "name": "Critical Value", + "summary": "\u003cp\u003e\u003c/p\u003e", + "url": "https://mastodon.social/@lambadalambda", + "manuallyApprovesFollowers": false, + "publicKey": { + "id": "https://mastodon.social/users/lambadalambda#main-key", + "owner": "https://mastodon.social/users/lambadalambda", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw0P/Tq4gb4G/QVuMGbJo\nC/AfMNcv+m7NfrlOwkVzcU47jgESuYI4UtJayissCdBycHUnfVUd9qol+eznSODz\nCJhfJloqEIC+aSnuEPGA0POtWad6DU0E6/Ho5zQn5WAWUwbRQqowbrsm/GHo2+3v\neR5jGenwA6sYhINg/c3QQbksyV0uJ20Umyx88w8+TJuv53twOfmyDWuYNoQ3y5cc\nHKOZcLHxYOhvwg3PFaGfFHMFiNmF40dTXt9K96r7sbzc44iLD+VphbMPJEjkMuf8\nPGEFOBzy8pm3wJZw2v32RNW2VESwMYyqDzwHXGSq1a73cS7hEnc79gXlELsK04L9\nQQIDAQAB\n-----END PUBLIC KEY-----\n" + }, + "tag": [], + "attachment": [], + "endpoints": { + "sharedInbox": "https://mastodon.social/inbox" + }, + "icon": { + "type": "Image", + "mediaType": "image/gif", + "url": "https://files.mastodon.social/accounts/avatars/000/000/264/original/1429214160519.gif" + }, + "image": { + "type": "Image", + "mediaType": "image/gif", + "url": "https://files.mastodon.social/accounts/headers/000/000/264/original/28b26104f83747d2.gif" + } +} diff --git a/test/fixtures/rel_me_anchor.html b/test/fixtures/rel_me_anchor.html new file mode 100644 index 000000000..5abcce129 --- /dev/null +++ b/test/fixtures/rel_me_anchor.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"/> + <title>Blog</title> + </head> + <body> + <article> + <h1>Lorem ipsum</h1> + <p>Lorem ipsum dolor sit ameph, …</p> + <a rel="me" href="https://social.example.org/users/lain">lain’s account</a> + </article> + </body> +</html> diff --git a/test/fixtures/rel_me_anchor_nofollow.html b/test/fixtures/rel_me_anchor_nofollow.html new file mode 100644 index 000000000..c856f0091 --- /dev/null +++ b/test/fixtures/rel_me_anchor_nofollow.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"/> + <title>Blog</title> + </head> + <body> + <article> + <h1>Lorem ipsum</h1> + <p>Lorem ipsum dolor sit ameph, …</p> + <a rel="me nofollow" href="https://social.example.org/users/lain">lain’s account</a> + </article> + </body> +</html> diff --git a/test/fixtures/rel_me_link.html b/test/fixtures/rel_me_link.html new file mode 100644 index 000000000..b9ff18f6e --- /dev/null +++ b/test/fixtures/rel_me_link.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"/> + <title>Blog</title> + <link rel="me" href="https://social.example.org/users/lain"/> + </head> + <body> + <article> + <h1>Lorem ipsum</h1> + <p>Lorem ipsum dolor sit ameph, …</p> + </article> + </body> +</html> diff --git a/test/fixtures/rel_me_null.html b/test/fixtures/rel_me_null.html new file mode 100644 index 000000000..5ab5f10c1 --- /dev/null +++ b/test/fixtures/rel_me_null.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"/> + <title>Blog</title> + </head> + <body> + <article> + <h1>Lorem ipsum</h1> + <p>Lorem ipsum dolor sit ameph, …</p> + <a rel="nofollow" href="https://social.example.org/users/lain">lain’s account</a> + </article> + </body> +</html> diff --git a/test/fixtures/rich_media/malformed-data.html b/test/fixtures/rich_media/malformed-data.html new file mode 100644 index 000000000..dec628c16 --- /dev/null +++ b/test/fixtures/rich_media/malformed-data.html @@ -0,0 +1,4874 @@ +
+
+
+
+
+
+
+
+<!DOCTYPE html>
+<html lang="pl">
+<head>
+
+
+
+
+
+<!-- canonical_start -->
+
+ <link rel="canonical" href="http://wyborcza.biz/biznes/7,147743,24417936,pomysl-na-biznes-chusta-ktora-chroni-przed-smogiem.html"/>
+
+
+<!-- canonical_end -->
+ +<!--10351348, [ /fix/modules/canonical.jsp ], canonicalModule--> +
+
+
+<script>
+ var activeSubscription = false;
+</script>
+
+<!-- playerInfoModule v1.0 -->
+
+
+
+
+ <META NAME="ROBOTS" CONTENT="NOARCHIVE">
+
+
+
+
+<!-- robotsModule v1.0 -->
+
+
+<title>Pomys na biznes: chusta, ktra chroni przed smogiem</title>
+<meta charset="ISO-8859-2">
+<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
+<link rel="shortcut icon" href="http://bi.gazeta.pl/im/5/7001/m7001335.gif">
+
+
+
+<link rel="apple-touch-icon-precomposed" sizes="144x144" href="http://static.im-g.pl/aliasy/foto/wyborcza/touch-icon-144x144.png">
+<link rel="apple-touch-icon-precomposed" sizes="114x114" href="http://static.im-g.pl/aliasy/foto/wyborcza/touch-icon-114x114.png">
+<link rel="apple-touch-icon-precomposed" sizes="72x72" href="http://static.im-g.pl/aliasy/foto/wyborcza/touch-icon-72x72.png">
+<link rel="apple-touch-icon-precomposed" href="http://static.im-g.pl/aliasy/foto/wyborcza/touch-icon-72x72.png">
+
+
+<!-- titleCharsetModule v1.0 -->
+
+
+
+
+
+
+
+ <!-- style preloader-test -->
+ <style> +img[data-src]:not(.loaded){position:absolute !important;top:0 !important;left:0 !important;display:block !important;width:0 !important;height:0 !important;visibility:hidden !important;font-size:0 !important}.preloaderContainer{position:relative;z-index:1;display:block;width:100%;height:100%;min-height:56px;background:#fff url("data:image/gif;base64,R0lGODlhJgA4APEDAP+AgP8AAP///wAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQFDQADACwAAAAAJgA4AAACcIyPicDt75RUsMKJjd0sY755E2iJElmZFPqoCdu6BxzJGt3ZAZ7bPKDb8YI/4lBXRB59S1mSiTNGlVMoTXqlZq0wbFcLE4jH5LL5jE6r1+y2+w2Py+f0uv2Oz+v3/L7/DxgoOEhYaHiImKi4yNjYVwAAIfkEBQ0AAgAsAAAAACYAHAAAAkaEj4nB7e+UVLDCiY3dLGO+eRNoiRJZmRT6qAnbugccyRrd2QCe23yg2/GCP+JQV0QefUtZkokzRpVTKE16pWatMGxXCysAACH5BAUNAAAALAAAAAATABwAAAIVlI+py+0Po5y02ouz3rz7D4biSCIFACH5BAUNAAEALBMAHAATABwAAAIVhI+py+0Po5y02ouz3rz7D4biSCIFACH5BAUNAAIALBMAAAATADgAAAIvhI+py+0Po5y02ouz3rz7D4biSCLBiabqyrbuC8fyTNf2jef6zvf+DwwKh8RiqgAAIfkEBQ0AAAAsEwAAABMAHAAAAhWUj6nL7Q+jnLTai7PevPsPhuJIIgUAIfkEBQ0AAQAsAAAcABMAHAAAAhWEj6nL7Q+jnLTai7PevPsPhuJIIgUAIfkEBQ0AAgAsAAAcACYAHAAAAkaMj4nA7e+UVLDCiY3dLGO+eRNoiRJZmRT6qAnbugccyRrd2QGe2zyg2/GCP+JQV0QefUtZkokzRpVTKE16pWatMGxXCysAACH5BAUNAAAALBMAHAATABwAAAIVlI+py+0Po5y02ouz3rz7D4biSCIFACH5BAUNAAEALAAAAAATABwAAAIVhI+py+0Po5y02ouz3rz7D4biSCIFACH5BAUNAAIALAAAAAATADgAAAIvjI+py+0Po5y02ouz3rz7D4biSCLAiabqyrbuC8fyTNf2jef6zvf+DwwKh8RiqgAAIfkEBQ0AAAAsAAAcABMAHAAAAhWUj6nL7Q+jnLTai7PevPsPhuJIIgUAOw==") center center no-repeat} + +</style>
+ <!-- /style preloader-test -->
+
+
+
+
+
+
+
+ <!-- head script -->
+ <script>function checkAdsElements(){if(void 0!==this.dfpArrLab&&this.dfpArrLab instanceof Array){for(var a=0;a<this.dfpArrLab.length;){var b=document.getElementById(this.dfpArrLab[a]);if(b){var c=b.clientHeight,d=b.querySelector("span.banLabel");c>40&&d.setAttribute("style","")}a++}return!0}return!1}function putBan(a){return!0}var wyborcza_pl=wyborcza_pl||{},gazeta_pl=gazeta_pl||{},_gaq=_gaq||[];this.dfpArrLab=["000-MAINBOARD","087-ADBOARD-A","087-ADBOARD-B","087-ADBOARD-C","087-ADBOARD-D","107-MAINBOARD-MOBI","150-ADBOARD-A-MOBI","150-ADBOARD-B-MOBI","150-ADBOARD-C-MOBI","150-ADBOARD-D-MOBI","003-RECTANGLE","035-RECTANGLE-BTF","042-FOOTBOARD","104-RECTANGLE-MOBI","108-FOOTBOARD-MOBI","ADBOARD-B","FOOTBOARDA"],document.addEventListener("DOMContentLoaded",function(a){setTimeout(function(){checkAdsElements()},1)}),wyborcza_pl.PlayerConfig={isMuted:!0},gazeta_pl.parseURI=function(){var a={},b=location.href.split(/\?/).pop().split(/&/),c=0;for(c=0;c<b.length;c++)b[c]=b[c].split(/=/),a[b[c].shift()]=b[c].join("=");return a},function(){var a=gazeta_pl.Player=gazeta_pl.Player||{resizeWrappers:["body","body > .main","#col_left","#col_right",".colLeft",".colRight"],jsonldMounted:0,init:function(){a.checkPageVisibilityPage(),a.initMessages(),a.initResizeHandler(),a.initShortcuts()},initMessages:function(){a.addEvent(window,"message",a.receiveMessage)},receiveMessage:function(b){var c=b.data;if(b.origin.match(/video\.gazeta\.pl$/)){try{c=JSON.parse(c)}catch(i){}if(c)switch(c.action){case"playerLoad":a.sendMessage(b.source,b.origin,{action:"playerInit",params:{mute:a.getMuteState()}}),a.resizeHandler();break;case"getAdParams":var d=window.AG&&window.AG.rodoAccepted||0,e={action:"setAdParams",referrer:document.referrer,rodoStatus:d};window.nobanner?e.nobanner=!0:"dfpParams"in window&&dfpParams&&dfpParams.video&&(e.url={preroll:dfpParams.video.preroll,postroll:dfpParams.video.postroll,inskin:dfpParams.video.skin,branding:dfpParams.video.skin,pausead:dfpParams.video.pausead,overlay:dfpParams.video.overlay}),a.sendMessage(b.source,b.origin,e);break;case"watchViewport":a.checkPlayerInViewport(b);break;case"resizeRequest":a.stretchSpace(b,c.width,c.height,function(c,d){a.sendMessage(b.source,b.origin,{action:"resizeDone",width:c,height:d})});break;case"hidePlayer":var f=a.findPlayerIframe(b);f&&(f.style.cssText+="display: none; visibility: hidden");break;case"getPlayerParams":var f=a.findPlayerIframe(b);a.sendMessage(b.source,b.origin,{action:"playerParams",pageUrl:document.URL,playerUrl:f?f.src:""});break;case"initFloatingPlayer":if(!window.jQuery)return;!a.FloatingPlayer.ready&&a.FloatingPlayer.isDesktopBrowser()&&!a.FloatingPlayer.isSafariBrowser()&&a.FloatingPlayer.isEmbededOnArticlePage()&&a.FloatingPlayer.init(),a.sendMessage(b.source,b.origin,{action:"setFloatingCapabilities",canFloat:!!a.FloatingPlayer.isFloatingPlayer(b.source)&&a.FloatingPlayer.ready});break;case"canPause":a.FloatingPlayer.ready&&a.FloatingPlayer.isFloatingPlayer(b.source)&&a.FloatingPlayer.closeButtonHandler(b.origin,c.canPauseState,c.playState);break;case"pushDataToDataLayer":a.pushDataToDataLayer(c.data);break;case"mediaInfo":if(!/\/1,|\/7,/.test(window.location.href)&&c.jsonld&&a.jsonldMounted<1){var g=document.querySelector("body"),h=document.createElement("script");h.setAttribute("type","application/ld+json"),h.innerHTML=JSON.stringify(c.jsonld),g.appendChild(h),a.jsonldMounted++}}}},playersInViewport:[],actualPlayed:null,sendMessage:function(a,b,c){a.postMessage(JSON.stringify(c),b)},stretchSpace:function(b,c,d,e){if(c){var f=a.findPlayerIframe(b);f&&(c&&(f.width=c),d&&(f.height=d),e(f.width,f.height))}},checkPlayerInViewport:function(b){if(b){new IntersectionObserver(function(c){c[0].intersectionRatio>=.5?a.enteredViewport(b):a.leavedViewport(b)},{threshold:[.5]}).observe(a.findPlayerIframe(b))}},checkPageVisibilityPage:function(){document.addEventListener("visibilitychange",function(){a.manageActivePlayer()})},manageActivePlayer:function(b,c){var d="visible"==document.visibilityState;if(!(!d||a.FloatingPlayer.ready&&a.actualPlayed)){var e=a.playersInViewport.length&&d?"enteredViewport":"leavedViewport";b=a.playersInViewport[0]?a.playersInViewport[0].source:b,c=a.playersInViewport[0]?a.playersInViewport[0].origin:c,b&&c&&(a.actualPlayed&&!gazeta_pl.playerStopsOutsideViewport&&a.actualPlayed[0]!=b&&a.sendMessage(a.actualPlayed[0],a.actualPlayed[1],{action:"leavedViewport"}),a.sendMessage(b,c,{action:e}),a.actualPlayed=[b,c])}},enteredViewport:function(b){b.origin,b.source;if(!gazeta_pl.playerStopsOutsideViewport&&!a.FloatingPlayer.ready)for(var c=0,d=a.playersInViewport.length;c<d;c++)a.sendMessage(a.playersInViewport[c].source,a.playersInViewport[c].origin,{action:"leavedViewport"});a.playersInViewport.push({source:b.source,origin:b.origin}),a.manageActivePlayer()},leavedViewport:function(b){for(var c=b.origin,d=b.source,e=(a.playersInViewport.length,a.playersInViewport.length-1);e>=0;e-=1)a.playersInViewport[e]&&a.playersInViewport[e].source==d&&a.playersInViewport.splice(e,1);(gazeta_pl.playerStopsOutsideViewport||a.playersInViewport.length)&&(a.FloatingPlayer.ready||a.manageActivePlayer(d,c))},initResizeHandler:function(){a.resizeHandler(),a.addEvent(window,"onorientationchange"in window?"orientationchange":"resize",a.resizeHandler)},resizeHandler:function(){clearTimeout(a.timeout),clearTimeout(a.fallbackTimeout),a.timeout=a.resizePlayer(50),a.fallbackTimeout=a.resizePlayer(500)},resizePlayer:function(b){var c=a.findPlayerIframes();return setTimeout(function(){for(var b=0;b<c.length;b++){var d=c[b],e=c[b].parentNode,f=980;e.getAttribute("data-pjs")&&(e.style.cssText="width: auto; height: auto",e=e.parentNode),f=Math.min(f,e.offsetWidth||f),a.resizeWrappers.forEach(function(a){document.querySelector(a)&&document.querySelector(a).querySelector('[src="'+d.src+'"]')&&(f=Math.min(f,document.querySelector(a).offsetWidth||f))}),f!=d.offsetWidth&&c[b].contentWindow.postMessage('{"action": "resize", "width": '+f+"}","*")}},b)},initShortcuts:function(){a.addEvent(window,"keydown",a.handleKeydown)},handleKeydown:function(b){var c="";b.altKey&&38==b.which?c="unmute":b.altKey&&40==b.which&&(c="mute"),c&&a.findPlayerIframes().forEach(function(b){a.sendMessage(b.contentWindow,"*",{action:c})})},addEvent:function(a,b,c){a.addEventListener?a.addEventListener(b,c,!1):a.attachEvent&&a.attachEvent("on"+b,function(){c.call(a,window.event)})},findPlayerIframes:function(){return Array.prototype.filter.call(document.getElementsByTagName("iframe"),function(a){return a.src&&a.src.match(/www\.gazeta\.tv\/plej\/19,/)})},findPlayerIframe:function(b){for(var c=a.findPlayerIframes(),d=0,e=c.length;d<e;d++)if(c[d].contentWindow===b.source)return c[d];return null},pushDataToDataLayer:function(a){"undefined"!=typeof dataLayer&&dataLayer.push(a)},getMuteState:function(){return!!(gazeta_pl.playerMute||a.isAutoplayMutedVideo()||void 0!==wyborcza_pl&&wyborcza_pl.PlayerConfig&&wyborcza_pl.PlayerConfig.isMuted)},isAutoplayMutedVideo:function(){var a=navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);return!!a&&parseInt(a[2],10)>=66},FloatingPlayer:{ready:!1,init:function(){var a,b,c,d,e,f,g,h=this;return a=$(window),b=$("body"),c=b.find("#gazeta_article_video"),c.length?(d=c.find('iframe[src*="//www.gazeta.tv/plej/19,"]'),e=b.height(),f={width:"30px",height:"30px",padding:"8.5px",position:"absolute",right:"0",top:"-40px",background:"#fff","border-radius":"3px","box-shadow":"0 0 20px 2px rgba(0, 0, 0, 0.3)","box-sizing":"border-box"},h.$closeButton=$('<div class="floatingCloseButton"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13 13"><polygon style="fill:#aaa;fill-rule:evenodd;clip-rule:evenodd;" points="12.9 1.6 11.4 0.2 6.5 5.1 1.5 0.2 0.1 1.6 5.1 6.5 0.1 11.5 1.5 12.9 6.5 7.9 11.4 12.9 12.9 11.5 7.9 6.5 "/></svg></div>'),h.$iframe=d,h.$iframeContainer=d.parent(),h.$mod=c,h.$window=a,h.$xIcon=h.$closeButton.find("polygon"),h.floated=!1,h.iframeHeight=d.height(),h.windowHeight=a.height(),h.$closeButton.css(f),e<=h.windowHeight+h.iframeHeight?h.ready=!1:(!h.floated&&h.isReadyToFloat()&&(h.floated=!0,h.floatPlayer()),h.$window.bind("scroll.floatingPlayer resize.floatingPlayer",function(a){g=!0}),h.intervalId=setInterval(function(){g&&(!h.floated&&h.isReadyToFloat()?(h.floated=!0,h.floatPlayer()):h.floated&&!h.isReadyToFloat()&&(h.floated=!1,h.floatPlayer()),g=!1)},100),h.ready=!0)):h.ready=!1},deinit:function(){var b=a.FloatingPlayer;clearInterval(b.intervalId),b.ready=!1,b.$window.unbind(".floatingPlayer"),b.floated=!1,b.floatPlayer()},floatPlayer:function(){var b,c,d,e,f,g,h,i,j=this,k=48,l=j.$iframe.width(),m=j.$mod[0].getBoundingClientRect(),n=j.$mod.height(),o=l/(j.$iframe.height()-k),p=j.$window.height(),q=j.$window.width();h=j.floated?300:document.getElementById("gazeta_article_video").offsetWidth,i=h/o+k,d={top:p-i-30-m.top-(j.floated?0:100),left:q-h-30-m.left},b={width:h,height:i},c={width:h,height:i,padding:"10px",position:"fixed",right:"20px",bottom:"20px",background:"#fff","border-radius":"3px","box-shadow":"0 0 20px 2px rgba(0, 0, 0, 0.3)","z-index":"2800"},e={width:h,height:i,position:"fixed",right:"30px",bottom:"30px","-webkit-transform":"translate(-"+d.left+"px,-"+d.top+"px)",transform:"translate(-"+d.left+"px,-"+d.top+"px)",transition:"all 0.2s linear","z-index":"2800"},f={width:h,height:i,position:"fixed",top:m.top,"-webkit-transform":"translate("+d.left+"px,"+d.top+"px)",transform:"translate("+d.left+"px,"+d.top+"px)",transition:"all 0.2s linear","z-index":"2800"},g={height:n,"background-color":"#f2f2f2","background-image":"url(//bi.im-g.pl/im/1/20781/m20781691,ZASLEPKA.png)","background-position":"bottom 30px right 30px","background-repeat":"no-repeat"},j.origin||(j.origin=window.location.protocol+"//video.gazeta.pl"),j.floated?(j.$mod.css(g),j.$iframe.css(f),j.$iframe.one("webkitTransitionEnd moztransitionend transitionend oTransitionEnd",function(a){j.$iframe.removeAttr("style"),j.$iframe.css(b),j.$iframeContainer.css(c),j.$iframeContainer.prepend(j.$closeButton)}),a.pushDataToDataLayer({category:"Plywajacy player",action:"Wyswietlenie",label:window.location.href,value:0,nonInteraction:!1,event:"zdarzenie"}),a.sendMessage(j.$iframe[0].contentWindow,j.origin,{action:"floatStatus",data:!0}),a.sendMessage(j.$iframe[0].contentWindow,j.origin,{action:"resize",width:300})):(j.$iframe.css(e),j.$iframeContainer.removeAttr("style"),j.$iframeContainer.find(".floatingCloseButton").detach(),j.$iframe.one("webkitTransitionEnd moztransitionend transitionend oTransitionEnd",function(a){j.$iframe.removeAttr("style"),j.$mod.removeAttr("style")}),a.sendMessage(j.$iframe[0].contentWindow,j.origin,{action:"floatStatus",data:!1}),a.sendMessage(j.$iframe[0].contentWindow,j.origin,{action:"resize",width:document.getElementById("gazeta_article_video").offsetWidth}))},closeFloatingPlayer:function(){var b=a.FloatingPlayer;if(void 0===b.$closeButton)return!1;a.sendMessage(b.$iframe[0].contentWindow,b.origin,{action:"callPause",data:"pause"}),b.deinit(),a.pushDataToDataLayer({category:"Plywajacy player",action:"Zamknij",label:window.location.href,value:0,nonInteraction:!1,event:"zdarzenie"})},closeButtonHandler:function(b,c,d){var e=a.FloatingPlayer;if(void 0===e.$closeButton)return!1;e.origin=b,e.playState=d,c||!c&&"play"!==d?(e.$xIcon.css("fill","#000"),e.$closeButton.css("cursor","pointer"),e.$closeButton.unbind("click",e.closeFloatingPlayer).bind("click",e.closeFloatingPlayer)):(e.$xIcon.css("fill","#aaa"),e.$closeButton.css("cursor","default"),e.$closeButton.unbind("click",e.closeFloatingPlayer))},isFloatingPlayer:function(b){var c=a.FloatingPlayer;return void 0!==c.$iframe&&c.$iframe[0].contentWindow===b},isReadyToFloat:function(){var b=a.FloatingPlayer;return b.$window.scrollTop()>b.$mod.offset().top+b.$mod.height()/4},isEmbededOnArticlePage:function(){return!!window.location.href.match(/\/[17],/)},isDesktopBrowser:function(){return void 0!==gazeta_pl.device?"NOT_MOBILE"==gazeta_pl.device:$("body").hasClass("desk")&&!$("body").hasClass("tablet")},isSafariBrowser:function(){return-1!=navigator.userAgent.indexOf("Safari")&&-1==navigator.userAgent.indexOf("Chrome")&&-1==navigator.userAgent.indexOf("Chromium")}}}}(),gazeta_pl.Player.init();</script>
+
+
+ <script type="text/javascript">var pfma_wyborcza_version = "1.1.16";</script>
+ <script type="text/javascript">var szpalty_wyborcza_version = "0.0.2";</script>
+ <!-- /head script -->
+
+
+
+<!-- preloaderModule v1.1 -->
+
+<script>
+ var now = new Date(2019, 0, 31, 17, 10, 30);
+</script>
+
+<!-- currentDateModule v1.0 -->
+
+
+<meta name="Description" content="Filtr ma tak dokadny, e zatrzymuje nawet wirusy i bakterie. Trjka wrocawian stworzya chust, ktra chroni przed smogiem. " />
+
+<!-- descriptionsModule v1.0 -->
+
+
+
+<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
+
+
+ <script>
+ //<![CDATA[
+ var gazeta_pl = gazeta_pl || {};
+
+ gazeta_pl.documentParam = {"root": "/biznes/"};
+
+ gazeta_pl.mobileInfo = {
+ "isMobileDevice": false
+ };
+
+
+
+ gazeta_pl.rootSectionId = 100791;
+
+ //]]>
+ </script>
+
+
+<!-- Bigdata -->
+<script src="https://bis.gazeta.pl/info/bluewhale/2.5.0/main-min.jsgz"></script> +
+
+
+<!-- portalDataModule v1.0 -->
+ + +<!-- - Agree rd - --> + + + +<!-- v4 rdGid: 1 --> +<!-- v4 Cookie : --> +<script src="//rodo.agora.pl/agreement/check?gid=1¶ms=&skipMessageOnReject=false"></script> + + + + + + + + <meta property="og:type" content="article" /> + + + + +<meta property="og:url" content="http://wyborcza.biz/biznes/7,147743,24417936,pomysl-na-biznes-chusta-ktora-chroni-przed-smogiem.html" /> + + + +<!-- Title --> +<meta property="og:title" + content="Pomys na biznes: chusta, ktra chroni przed smogiem" /> +<meta name="twitter:title" + content="Pomys na biznes: chusta, ktra chroni przed smogiem" /> + + + <!-- DESC --> + <meta property="og:description" + content="Filtr ma tak dokadny, e zatrzymuje nawet wirusy i bakterie. Trjka wrocawian stworzya chust, ktra chroni przed smogiem. " /> + <meta name="twitter:description" + content="Filtr ma tak dokadny, e zatrzymuje nawet wirusy i bakterie. Trjka wrocawian stworzya chust, ktra chroni przed smogiem. " /> + + + + + + + + + + <!-- IMAGE --> + <meta property="og:image" content="https://bi.im-g.pl/im/f7/49/17/z24418295FBW,Prace-nad-projektem-chusty-antysmogowej-rozpoczely.jpg" /> + <meta name="twitter:image" content="https://bi.im-g.pl/im/f7/49/17/z24418295FBW,Prace-nad-projektem-chusty-antysmogowej-rozpoczely.jpg" /> + <meta name="twitter:card" content="summary_large_image" /> + + + + <meta property="og:site_name" content="wyborcza.biz" /> + + + + <meta property="fb:app_id" content="515714931781741" /> + + + + +<meta property="og:locale" content="pl_PL" /> + + + +<!--facebookIdFanpages start --> +<meta property="fb:pages" content="182216700504" /> +<meta property="fb:pages" content="63563473556" /> +<meta property="fb:pages" content="211137058789" /> +<meta property="fb:pages" content="158571784110" /> +<meta property="fb:pages" content="1123290301110277" /> +<meta property="fb:pages" content="110402292339268" /> +<meta property="fb:pages" content="228168274881" /> +<meta property="fb:pages" content="134754663619446" /> +<meta property="fb:pages" content="174480419336" /> +<meta property="fb:pages" content="116460718114" /> +<meta property="fb:pages" content="172897561400" /> +<meta property="fb:pages" content="297584282665" /> +<meta property="fb:pages" content="115721688487368" /> +<meta property="fb:pages" content="178164578911" /> +<meta property="fb:pages" content="336438736394843" /> +<meta property="fb:pages" content="145218379355757" /> +<meta property="fb:pages" content="217794324954867" /> +<meta property="fb:pages" content="145231052239934" /> +<meta property="fb:pages" content="259626760890136"> +<meta property="fb:pages" content="224446291000703" /> +<meta property="fb:pages" content="169554536536974" /> +<meta property="fb:pages" content="189371001102606" /> +<meta property="fb:pages" content="202936473194614" /> +<meta property="fb:pages" content="750649501650020" /> +<meta property="fb:pages" content="732718100095535" /> +<meta property="fb:pages" content="212900872097857" /> +<meta property="fb:pages" content="451218768269373" /> +<meta property="fb:pages" content="164720961114" /> +<meta property="fb:pages" content="1472966049586410" /> +<meta property="fb:pages" content="212665112154600" /> +<meta property="fb:pages" content="186699381425534" /> +<meta property="fb:pages" content="701673749899603" /> +<meta property="fb:pages" content="182114553798" /> +<meta property="fb:pages" content="1509388655954951" /> +<meta property="fb:pages" content="212912818721293" /> +<meta property="fb:pages" content="346589322185" /> +<meta property="fb:pages" content="149902472546" /> +<meta property="fb:pages" content="282927535056444" /> +<meta property="fb:pages" content="196338433729597" /> +<meta property="fb:pages" content="133385026676460" /> +<meta property="fb:pages" content="354565604586" /> +<meta property="fb:pages" content="277864075631246" /> +<meta property="fb:pages" content="175270922504122" /> +<meta property="fb:pages" content="211916968882547" /> +<meta property="fb:pages" content="127659060591519" /> +<meta property="fb:pages" content="217981241618571" /> +<meta property="fb:pages" content="198606720154883" /> +<meta property="fb:pages" content="105171796186333" /> +<meta property="fb:pages" content="1800655190193744" /> +<meta property="fb:pages" content="501324603247131" /> +<meta property="fb:pages" content="159338220786135" /> +<meta property="fb:pages" content="151677518189583" /> +<meta property="fb:pages" content="302385839814255" /> +<meta property="fb:pages" content="200768423330804" /> +<meta property="fb:pages" content="288018984602680" /> +<!--facebookIdFanpages end --> + + +<!-- facebookModule v1.1 --> +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <!-- (C)2000-2014 Gemius SA - gemiusAudience -->
+ <script type="text/javascript">
+ <!--//--><![CDATA[//><!--
+ var pp_gemius_identifier = new String('citFBk7kdQKEPMwqCmPRXqSc.IlGzFiuWwxH9NXy4Zb.a7/arg=147743');
+
+ function gemius_pending(i) { window[i] = window[i] || function() {var x = window[i+'_pdata'] = window[i+'_pdata'] || []; x[x.length]=arguments;};};
+ setTimeout(function() {
+ gemius_pending('gemius_hit'); gemius_pending('gemius_event'); gemius_pending('pp_gemius_hit'); gemius_pending('pp_gemius_event');
+ (function(d,t) {try {var gt=d.createElement(t),s=d.getElementsByTagName(t)[0],l=((location.protocol=='https:')?'https://bis.gazeta.pl/info/http/xgemius-min.jsgz':'http://static.gazeta.pl/info/http/xgemius-min.jsgz'); gt.setAttribute('async','async');
+ gt.setAttribute('defer','defer'); gt.src=l; s.parentNode.insertBefore(gt,s);} catch (e) {}})(document,'script');
+ }, 50);
+ //--><!]]>
+ </script>
+
+
+
+<!-- gemiusModule v1.1 -->
+
+
+
+<meta name="Keywords" content="Konsument i zakupy">
+
+ <meta name="news_keywords" content="Konsument i zakupy">
+
+
+<!-- keywordsModule v1.0 -->
+
+
+
+
+<meta name="google-site-verification" content="3P4BE3hLw82QWqtseIE60qQcOtrpMxMnCNkcv62pjTA" />
+
+
+
+ <!-- Google Tag Manager --> <noscript><iframe src="//www.googletagmanager.com/ns.html?id=GTM-W4DZP5" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript> <script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= '//www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); })(window,document,'script','dataLayer','GTM-W4DZP5');</script> <!-- End Google Tag Manager -->
+
+
+
+
+<!-- googleModule v1.0 -->
+
+
+
+
+
+
+ <meta name="viewport" content="width=device-width, user-scalable=yes">
+
+
+
+<!-- viewportModule v1.0 -->
+
+
+
+
+
+
+
+ <!-- wyborcza_common_style not set -->
+
+
+ <link rel="stylesheet" href="//static.im-g.pl/css/wyborcza-common/builds/18.0.6/style-min.cssgz" />
+
+
+
+
+
+
+<!-- wyborcza_common_style /css/wyborcza-biz/version-mobile.htm -->
+<!-- wyborcza_common_style /css/wyborcza-biz/version-desktop.htm -->
+
+
+ <link rel="stylesheet" href="//static.im-g.pl/wyborcza/wyborcza-biz/css/1.0.9/style-min.cssgz" />
+
+
+
+
+<!-- stylesModule v1.0 -->
+
+
+
+
+ <link rel="alternate" type="application/rss+xml" title="Wiadomoci gospodarcze" href="http://wyborcza.biz/pub/rss/wyborcza_biz_wiadomosci.htm">
+
+
+<!-- rssModule v1.0 -->
+
+
+<!-- Facebook Pixel Code -->
+<script>
+ !function(f, b, e, v, n, t, s) {
+ if (f.fbq)
+ return;
+ n = f.fbq = function() {
+ n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments)
+ };
+ if (!f._fbq)
+ f._fbq = n;
+ n.push = n;
+ n.loaded = !0;
+ n.version = '2.0';
+ n.queue = [];
+ t = b.createElement(e);
+ t.async = !0;
+ t.src = v;
+ s = b.getElementsByTagName(e)[0];
+ s.parentNode.insertBefore(t, s)
+ } (window, document, 'script', 'https://connect.facebook.net/en_US/fbevents.js');
+ fbq('init', '534530363346991'); // Insert your pixel ID here.
+ fbq('track', 'PageView');
+</script>
+
+<noscript>
+ <img height="1" width="1" style="display: none" src="https://www.facebook.com/tr?id=534530363346991&ev=PageView&noscript=1" />
+</noscript>
+
+<!-- DO NOT MODIFY -->
+<!-- End Facebook Pixel Code -->
+
+<!-- facebookPixelModule v1.0 -->
+
+
+<script>
+ var wyborcza_pl = wyborcza_pl || {};
+ wyborcza_pl.LokaleGeolocationCityId = '';
+</script>
+
+<!-- geolocationModule v1.0 -->
+
+
+
+
+
+<!-- browserNotificationEnableModule -->
+
+
+
+
+
+
+
+ <script type="text/javascript">
+ var _sf_async_config = _sf_async_config || {};
+ _sf_async_config.uid = 14371;
+ _sf_async_config.domain = 'wyborcza.biz';
+ _sf_async_config.flickerControl = false;
+
+
+
+ _sf_async_config.useCanonical = true;
+
+
+
+
+ var _sf_startpt = (new Date()).getTime();
+ </script>
+
+ <script async src="//static.chartbeat.com/js/chartbeat_mab.js"></script>
+
+
+<!-- chartbeatHeadModule v1.0 -->
+ +<!--10184895, [ null ], aggregatorModule--> +
+
+
+
+
+ + + + + + + +<script type='text/javascript'> + + + + + + + + + + + + + + + + + + + + + + + + + + if(!window.AG){ + window.AG = window.AG || {}; + window.AG.rodoAccepted = -1; +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + var + adviewDFP = adviewDFP || {}; +adviewDFP.DFPTargeting = []; + + + + + + // [HB] Core-prod-1 - HEADER_START +var PREBID_TIMEOUT = (window.AG && window.AG.rodoAccepted === 1) ? 500 : 0; +var pbjs_currency = 1; // -> to $1 +var pbjs_ga = true; +var pbjs_yb_hb = false; +var eur2usd = 1.1; +var yb_dosamplerate = 100; +var yb_seg = ''; +var _st0=new Date(); + +// BIG data +BigData = { + notify: function(eventName){ + var event = BigData.createEvent(eventName); + BigData.fireEvent(event); + }, + createEvent: function(eventName) { + var event; + if (document.createEvent) { + event = document.createEvent("HTMLEvents"); + event.initEvent(eventName, true, true); + } else { + event = document.createEventObject(); + event.eventType = eventName; + } + event.eventName = eventName; + return event; + }, + fireEvent: function(event) { + if (document.createEvent) { + document.dispatchEvent(event); + } else { + document.fireEvent("on" + event.eventType, event); + } + } +}; +//bwGuidv2 +function getCookie(cname) { + var name = cname + "="; + var ca = document.cookie.split(';'); + for(var i=0; i<ca.length; i++) { + var c = ca[i]; + while (c.charAt(0)==' ') c = c.substring(1); + if (c.indexOf(name) == 0) return c.substring(name.length, c.length); + } + return ""; +} +function getJSON (url, callback) { + var xhr = new XMLHttpRequest(); + xhr.open("get", url, true); + xhr.responseType = "json"; + xhr.onload = function() { + var status = xhr.status; + if (status == 200) { + callback(null, xhr.response); + } else { + callback(status); + } + }; + xhr.send(); +}; +//***************************************** + +if(Math.random()*100 <= yb_dosamplerate ) { + var bwGuidv2 = getCookie("bwGuidv2"); + if (bwGuidv2 != "") { + getJSON("https://squid.gazeta.pl/do/p/v1/gpfgo?g=" + bwGuidv2 + "&p=30000", + function(err, data) { + if (err != null) { + console.log("getJSON: Something went wrong: " + err); + } else { + try{ + var segments = []; + for(index in data.profile.features){ + segments.push(String(index)); + } + yb_seg = segments; + }catch(e){} + } + }); + } +} +document.write('\x3Cscript type="text/javascript" src="https://bi.adview.pl/static/adview/front/master/dfp/tools/adview/prebid.js">\x3C/script>'); + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + // 20181218 + +var adUnits = [{ + //wyborcza.biz_001-TOPBOARD + code: 'div-gpt-ad-001-TOPBOARD-0', + sizes: [[970,250]], + bids: [{ + bidder: 'appnexus', + params: { placementId: '11549538' } + },{ + bidder: 'adform', + params: { mid: '427595' } + },{ + bidder: 'pubmatic', + params: { publisherId: '155949', adSlot: 'wyborcza.biz_001-TOPBOARD@970x250' } + },{ + bidder: 'ix', + params: { size: [970,250], siteId: '227220' } + },{ + bidder: 'rtbhouse', + params: { region: 'prebid-eu', publisherId: 'da39a3ee5e6b4b0d' } + },{ + bidder: 'connectad', + params: { networkId: '10047', siteId: '1013785' } + }] +},{ + //wyborcza.biz_003-RECTANGLE + code: 'div-gpt-ad-003-RECTANGLE-0', + sizes: [[300,600]], + bids: [{ + bidder: 'appnexus', + params: { placementId: '11549537' } + },{ + bidder: 'adform', + params: { mid: '427594' } + },{ + bidder: 'pubmatic', + params: { publisherId: '155949', adSlot: 'wyborcza.biz_003-RECTANGLE@300x600' } + },{ + bidder: 'ix', + params: { size: [300,600], siteId: '227219' } + },{ + bidder: 'rtbhouse', + params: { region: 'prebid-eu', publisherId: 'da39a3ee5e6b4b0d' } + },{ + bidder: 'connectad', + params: { networkId: '10047', siteId: '1013784' } + }] +}]; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + // [HB] Core-prod-2 - OVER_GT +if(pbjs_yb_hb){ + var yb_hb = (pbjs_yb_hb ? Math.round(Math.random()) : 1); + PREBID_TIMEOUT *= yb_hb; + } + var pbjs = pbjs || {}; + pbjs.que = pbjs.que || []; + pbjs.que.push(function() { + for (var i = 0; i < adUnits.length; i++){shuffle(adUnits[i].bids);} + if(PREBID_TIMEOUT>0) pbjs.addAdUnits(adUnits); + + /* pass consent strings - START */ + + getCookie = function (name) { + var value = "; " + document.cookie; + var parts = value.split("; " + name + "="); + if (parts.length == 2) return parts.pop().split(";").shift(); + } + + pbjs.setConfig({ + consentManagement: { + cmpApi: 'static', + allowAuctionWithoutConsent: true, + consentData: { + getConsentData: { + 'gdprApplies': false, + 'hasGlobalScope': false, + 'consentData': getCookie('euconsent') + }, + getVendorConsents: { + 'metadata': getCookie('euconsent') + } + } + } + }); + /* pass consent string - END */ + + pbjs.requestBids({ + bidsBackHandler: function(bidResponses) { + pbjs.dataPrebidReady=true; + BigData.notify("data-prebid-ready"); + } + }); + pbjs.bidderSettings = { + standard: { + adserverTargeting: [{ + key: "hb_bidder", val: function(bidResponse) { return bidResponse.bidderCode; }},{ + key: "hb_adid", val: function(bidResponse) { return bidResponse.adId; }},{ + key: "hb_pb", val: function(bidResponse) { return (bidResponse.pbHg*pbjs_currency).toFixed(2); }},{ + key: "hb_size", val: function(bidResponse) { return bidResponse.size; } + }] + }, + audienceNetwork: { bidCpmAdjustment : function(bidCpm){ return (bidCpm * 0.75); }}, + districtmDMX: { bidCpmAdjustment : function(bidCpm){ return (bidCpm * 0.85); }}, + aol: { bidCpmAdjustment : function(bidCpm){ return (bidCpm * 0.8); }}, + adform: { bidCpmAdjustment : function(bidCpm){ return (bidCpm * 0.9)*eur2usd; }}, + rubicon: { bidCpmAdjustment : function(bidCpm){ return (bidCpm * 0.82)*eur2usd; }}, + smartadserver: { bidCpmAdjustment : function(bidCpm){ return (bidCpm * 0.9); }}, + pubmatic: { bidCpmAdjustment : function(bidCpm){ return (bidCpm * 0.9); }}, + appnexusAst: { bidCpmAdjustment : function(bidCpm){ return (bidCpm * 0.65); }} + }; + if(pbjs_ga) { + (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ + (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), + m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) + })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); + ga('create', 'UA-84607356-7', 'auto', 'yb', {'sampleRate': 0.1}); + pbjs.enableAnalytics({ + provider: 'ga', + options: { + global: 'ga', // String name of GA global. Default is 'ga' + enableDistribution: false, + trackerName: 'yb' + } + }); + } + }); + function shuffle(a) { + var i = 0, j = 0, temp = null; + for (i = a.length-1; i > 0; i--) { + j = Math.floor(Math.random() * (i + 1)); + temp = a[i]; + a[i] = a[j]; + a[j] = temp; + } + } + +var lazyGPT = true; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + var googletag = googletag || {}; + googletag.cmd = googletag.cmd || []; + var lazyGPT = lazyGPT || false; + + if(!lazyGPT) { + + (function() { + var useSSL = 'https:' == document.location.protocol; + var src = (useSSL ? 'https:' : 'http:') + + '//www.googletagservices.com/tag/js/gpt.js'; + document.write('<scr' + 'ipt src="' + src + '"></scr' + 'ipt>'); + })(); + } + + var dfpParams = dfpParams || {}; + dfpParams.video = dfpParams.video || {}; + dfpParams.slots = dfpParams.slots || {}; + dfpParams.jsp = 23; + dfpParams.dir = 'biznes'; + dfpParams.dx = '147743'; + + dfpParams.prefix = '/75224259/AGORA-IN/Wyborcza.biz'; + + + dfpParams.video.preroll = '//pubads.g.doubleclick.net/gampad/ads?sz=400x300|640x480&iu=/75224259/AGORA-IN/Wyborcza.biz/090-PREROLL&cust_params=pos%3D090-PREROLL%26dx%3D147743%26jsp%3D23%26dir%3Dbiznes%26kw%3D[brandsafe]%26dystrybutor%3D[distributor_id]%26passback_id%3D[passback_id]%26domena%3D[adview_hostname]%26cb%3D[cb]&url=[locationhref]&description_url=[locationhref]&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&correlator=[timestamp]'; + + dfpParams.video.skin = '//pubads.g.doubleclick.net/gampad/ads?sz=400x300|640x480&iu=/75224259/AGORA-IN/Wyborcza.biz/120-SKIN&cust_params=pos%3D120-SKIN%26dx%3D147743%26jsp%3D23%26dir%3Dbiznes%26kw%3D[brandsafe]%26dystrybutor%3D[distributor_id]%26passback_id%3D[passback_id]%26domena%3D[adview_hostname]%26cb%3D[cb]&url=[locationhref]&description_url=[locationhref]&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&correlator=[timestamp]'; + + dfpParams.video.overlay = '//pubads.g.doubleclick.net/gampad/ads?sz=400x300|640x480&iu=/75224259/AGORA-IN/Wyborcza.biz/123-OVERLAY&cust_params=pos%3D123-OVERLAY%26dx%3D147743%26jsp%3D23%26dir%3Dbiznes%26kw%3D[brandsafe]%26dystrybutor%3D[distributor_id]%26passback_id%3D[passback_id]%26domena%3D[adview_hostname]%26cb%3D[cb]&url=[locationhref]&description_url=[locationhref]&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&correlator=[timestamp]'; + + dfpParams.video.pausead = '//pubads.g.doubleclick.net/gampad/ads?sz=400x300|640x480&iu=/75224259/AGORA-IN/Wyborcza.biz/126-PAUSEAD&cust_params=pos%3D126-PAUSEAD%26dx%3D147743%26jsp%3D23%26dir%3Dbiznes%26kw%3D[brandsafe]%26dystrybutor%3D[distributor_id]%26passback_id%3D[passback_id]%26domena%3D[adview_hostname]%26cb%3D[cb]&url=[locationhref]&description_url=[locationhref]&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&correlator=[timestamp]'; + + + + + dfpParams.slots['021-IMK'] = { url: "fif.htm?adUnit=/75224259/AGORA-IN/Wyborcza.biz/021-IMK&adUnitCode=021-IMK&adUnitSize=[[300,90],'fluid']&dx=147743&dir=biznes&jsp=23", sizes: [[300,90],'fluid'], autoLoad: false, autoLoadMargin: 0 }; + + dfpParams.slots['003-RECTANGLE'] = { url: "fif.htm?adUnit=/75224259/AGORA-IN/Wyborcza.biz/003-RECTANGLE&adUnitCode=003-RECTANGLE&adUnitSize=[[300,250],[300,600],[160,600],[120,600]]&dx=147743&dir=biznes&jsp=23", sizes: [[300,250],[300,600],[160,600],[120,600]], autoLoad: false, autoLoadMargin: 200 }; + + dfpParams.slots['007-CONTENTBOARD'] = { url: "fif.htm?adUnit=/75224259/AGORA-IN/Wyborcza.biz/007-CONTENTBOARD&adUnitCode=007-CONTENTBOARD&adUnitSize=[[300,250],[320,250],[336,280],[620,200],'fluid']&dx=147743&dir=biznes&jsp=23", sizes: [[300,250],[320,250],[336,280],[620,200],'fluid'], autoLoad: false, autoLoadMargin: 200 }; + + dfpParams.slots['071-WINIETA'] = { url: "fif.htm?adUnit=/75224259/AGORA-IN/Wyborcza.biz/071-WINIETA&adUnitCode=071-WINIETA&adUnitSize=[[940,140],[940,70]]&dx=147743&dir=biznes&jsp=23", sizes: [[940,140],[940,70]], autoLoad: false, autoLoadMargin: 200 }; + + dfpParams.slots['035-RECTANGLE-BTF'] = { url: "fif.htm?adUnit=/75224259/AGORA-IN/Wyborcza.biz/035-RECTANGLE-BTF&adUnitCode=035-RECTANGLE-BTF&adUnitSize=[[300,250]]&dx=147743&dir=biznes&jsp=23", sizes: [[300,250]], autoLoad: false, autoLoadMargin: 0 }; + + dfpParams.slots['067-RECTANGLE-BTF'] = { url: "fif.htm?adUnit=/75224259/AGORA-IN/Wyborcza.biz/067-RECTANGLE-BTF&adUnitCode=067-RECTANGLE-BTF&adUnitSize=[[300,250]]&dx=147743&dir=biznes&jsp=23", sizes: [[300,250]], autoLoad: false, autoLoadMargin: 0 }; + + dfpParams.slots['091-RELATED'] = { url: "fif.htm?adUnit=/75224259/AGORA-IN/Wyborcza.biz/091-RELATED&adUnitCode=091-RELATED&adUnitSize=[[155,290],'fluid']&dx=147743&dir=biznes&jsp=23", sizes: [[155,290],'fluid'], autoLoad: false, autoLoadMargin: 200 }; + + dfpParams.slots['019-TOPLAYER'] = { url: "fif.htm?adUnit=/75224259/AGORA-IN/Wyborcza.biz/019-TOPLAYER&adUnitCode=019-TOPLAYER&adUnitSize=[]&dx=147743&dir=biznes&jsp=23", sizes: [], autoLoad: false, autoLoadMargin: 200 }; + + dfpParams.slots['001-TOPBOARD'] = { url: "fif.htm?adUnit=/75224259/AGORA-IN/Wyborcza.biz/001-TOPBOARD&adUnitCode=001-TOPBOARD&adUnitSize=[[728,90],[750,100],[750,200],[750,300],[940,300],[970,250],[1170,300]]&dx=147743&dir=biznes&jsp=23", sizes: [[728,90],[750,100],[750,200],[750,300],[940,300],[970,250],[1170,300]], autoLoad: false, autoLoadMargin: 200 }; + + dfpParams.slots['042-FOOTBOARD'] = { url: "fif.htm?adUnit=/75224259/AGORA-IN/Wyborcza.biz/042-FOOTBOARD&adUnitCode=042-FOOTBOARD&adUnitSize=[[728,90],[750,100],[750,200],[750,300],[940,300],[970,250],[940,1200]]&dx=147743&dir=biznes&jsp=23", sizes: [[728,90],[750,100],[750,200],[750,300],[940,300],[970,250],[940,1200]], autoLoad: true, autoLoadMargin: 20 }; + + + + +</script> + +<!-- IBA Analytics --> +<!-- 20180116 --> +<script type="text/javascript"> + var IBA=IBA||{};IBA.dfp={adUnits:[],rendered:[],exmplUrl:null,onSlotRendered:function(t){null===IBA.dfp.exmplUrl&&(IBA.dfp.exmplUrl=t.slot.ba);var e=t.slot.getAdUnitPath();IBA.dfp.adUnits.push(e),t.isEmpty===!1&&IBA.dfp.rendered.push(e),IBA.adb.update(),"4"==IBA.adb.status&&IBA.adb.onReady()},update:function(){var t=[],e=[];if("undefined"!=typeof googletag&&"function"==typeof googletag.pubads&&"undefined"!=typeof googletag.pubads().getSlots)for(var a=googletag.pubads().getSlots(),n=0;n<a.length;n++)null!==a[n].J&&e.push(a[n].getAdUnitPath()),t.push(a[n].getAdUnitPath());e.length>IBA.dfp.rendered.length&&(IBA.dfp.rendered=e),t.length>IBA.dfp.adUnits.length&&(IBA.dfp.adUnits=t),IBA.adb.onReady()},testUrlAndDispatch:function(){var t=!1,e=new XMLHttpRequest;if(e.open("HEAD",IBA.dfp.exmplUrl,!0),e.onreadystatechange=function(){console.log(e.readyState+" | status| "+e.status),0===e.status&&(IBA.adb.status="5"),IBA.adb.dispatch()},IBA.dfp.exmplUrl)try{e.send(),t=!0}catch(a){IBA.adb.status="5",IBA.adb.dispatch()}else console.warn("IBA exmplUrl"+IBA.dfp.exmplUrl)}},IBA.adb={states:{0:"Not available",1:"Partially available but not rendered",2:"Partially available at least one rendered",3:"Available but not rendered",4:"Available at least one rendered",5:"Not available but lib loaded",9:"Default : unknown"},gem:{0:"AfHgvarrP4PC7igzOLWh7dVpTGP109BuRIffksuYjfT.c7",1:"ofhA3bsKT7xY8Cgj4cuRX8cQ.qiY_fsP1VURxgCyYv3.F7",2:"ofhA3bsKT7xY8Cgj4cuRX8cQ.qiY_fsP1VURxgCyYv3.F7",3:"nF5K4QQDK9TNGw_ql.fdc9U7XrD1irt.GoVhWXaFu6b.Z7",4:"nF5K4QQDK9TNGw_ql.fdc9U7XrD1irt.GoVhWXaFu6b.Z7",5:"ofhA3bsKT7xY8Cgj4cuRX8cQ.qiY_fsP1VURxgCyYv3.F7",9:"__unknown__"},status:"9",update:function(){var t=IBA.dfp.adUnits.length,e=IBA.dfp.rendered.length,a=window.abp||!1;0===t?IBA.adb.status="0":0===e&&a===!0?IBA.adb.status="1":e>0&&a===!0?IBA.adb.status="2":0===e?IBA.adb.status="3":e>0&&(IBA.adb.status="4")},onReady:function(){IBA.adb.ready!==!0&&(IBA.adb.ready=!0,IBA.adb.update(),"3"==IBA.adb.status?IBA.dfp.testUrlAndDispatch():IBA.adb.dispatch())},ready:!1,dispatch:function(){var t=IBA.adb.status;window.gemius_identifier=IBA.adb.gem[t];var e=document.createElement("script"),a=document.getElementsByTagName("script")[0];e.onerror=function(t){},e.src="//gazeta.hit.gemius.pl/gemius.js",a.parentNode.insertBefore(e,a),IBA.BigData.notify("adblockdataready")}},IBA.BigData={notify:function(t){var e=IBA.BigData.createEvent(t);IBA.BigData.fireEvent(e)},createEvent:function(t){var e;return document.createEvent?(e=document.createEvent("HTMLEvents"),e.initEvent(t,!0,!0)):(e=document.createEventObject(),e.eventType=t),e.eventName=t,e},fireEvent:function(t){document.createEvent?document.dispatchEvent(t):document.fireEvent("on"+t.eventType,t)}},setTimeout(function(){try{"complete"===document.readyState?IBA.dfp.update():window.addEventListener("load",IBA.dfp.update,!1)}catch(t){}},20); +</script> + + + + + + + + + <!-- eyeoH --> +<!-- eyeoHeadScripts 1.0 --> +<script type='text/javascript'>var abp;</script> +<script type='text/javascript' src='/aliasy/adp/px.js?ch=1'></script> +<script type='text/javascript' src='/aliasy/adp/px.js?ch=2'></script> +<!-- /eyeoH --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <script type='text/javascript'> + googletag.cmd.push(function() { + + var mobileSizeMap = googletag.sizeMapping() + .addSize([336,0], [[1,1],[300,250],[320,250],[300,150],[320,150],[336,280],[267,310],[155,290],[320,100],[320,50],[320,125],[320,266],[320,300],[200,130],[300,600], 'fluid']) + .addSize([0,0], [[1,1],[300,250],[320,250],[300,150],[320,150],[267,310],[155,290],[320,100],[320,50],[320,125],[320,266],[320,300],[200,130],[300,600], 'fluid']) + .build(); + + + + + var mobileSizeMap_101_topboard_mobi = googletag.sizeMapping().addSize([336,0], [[300,250],[320,250],[300,150],[320,150],[336,280]]).addSize([0,0], [[300,250],[320,250],[300,150],[320,150]]).build(); + + var mobileSizeMap_104_rectangle_mobi = googletag.sizeMapping().addSize([336,0], [[300,250],[320,250],[300,150],[320,150],[336,280],[300,600],'fluid']).addSize([0,0], [[300,250],[320,250],[300,150],[320,150],[300,600],'fluid']).build(); + + var mobileSizeMap_108_footboard_mobi = googletag.sizeMapping().addSize([336,0], [[300,250],[320,250],[300,150],[320,150],[336,280]]).addSize([0,0], [[300,250],[320,250],[300,150],[320,150]]).build(); + + var mobileSizeMap_091_related = googletag.sizeMapping().addSize([336,0], [[155,290],'fluid']).addSize([0,0], [[155,290],'fluid']).build(); + + var mobileSizeMap_150_adboard_a_mobi = googletag.sizeMapping().addSize([336,0], [[300,250],[320,250],[300,150],[320,150],[336,280]]).addSize([0,0], [[300,250],[320,250],[300,150],[320,150]]).build(); + + var mobileSizeMap_019_toplayer_mobi = googletag.sizeMapping().addSize([336,0], []).addSize([0,0], []).build(); + + + + + googletag.defineSlot('/75224259/AGORA-IN/Wyborcza.biz/001-TOPBOARD', [[728,90],[750,100],[750,200],[750,300],[940,300],[970,250],[1170,300]], 'div-gpt-ad-001-TOPBOARD-0').setTargeting('pos', ['001-TOPBOARD']).addService(googletag.pubads()); +googletag.defineSlot('/75224259/AGORA-IN/Wyborcza.biz/003-RECTANGLE', [[300,250],[300,600],[160,600],[120,600]], 'div-gpt-ad-003-RECTANGLE-0').setTargeting('pos', ['003-RECTANGLE']).addService(googletag.pubads()); +googletag.defineSlot('/75224259/AGORA-IN/Wyborcza.biz/067-RECTANGLE-BTF', [[300,250]], 'div-gpt-ad-067-RECTANGLE-BTF-0').setTargeting('pos', ['067-RECTANGLE-BTF']).addService(googletag.pubads()); +googletag.defineSlot('/75224259/AGORA-IN/Wyborcza.biz/071-WINIETA', [[940,140],[940,70]], 'div-gpt-ad-071-WINIETA-0').setTargeting('pos', ['071-WINIETA']).addService(googletag.pubads()); +googletag.defineSlot('/75224259/AGORA-IN/Wyborcza.biz/007-CONTENTBOARD', [[300,250],[320,250],[336,280],[620,200],'fluid'], 'div-gpt-ad-007-CONTENTBOARD-0').setTargeting('pos', ['007-CONTENTBOARD']).addService(googletag.pubads()); +googletag.defineSlot('/75224259/AGORA-IN/Wyborcza.biz/035-RECTANGLE-BTF', [[300,250]], 'div-gpt-ad-035-RECTANGLE-BTF-0').setTargeting('pos', ['035-RECTANGLE-BTF']).addService(googletag.pubads()); + + + googletag.pubads().setTargeting('dx','147743'); + + googletag.pubads().setTargeting('dir','biznes'); + + googletag.pubads().setTargeting('jsp','23'); + + googletag.pubads().setTargeting('cb', [''+window.AG.rodoAccepted]); + googletag.pubads().setTargeting('uponit','false'); + + + + + + + + + var _YB=_YB||{ab:function(){return (Math.random()>=0.1?"b":"a"+Math.floor(Math.random()*10));}}; + googletag.pubads().getSlots().forEach(function(slot){slot.setTargeting('yb_ab', _YB.ab());}); + + + + var + cookieIfr = null; +if (document.referrer) { + + var + url_domain = function(data) { + var a = document.createElement('a'); + a.href = data; + return a.protocol + '//' + a.hostname; + }; + + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (this.readyState == 4 && this.status == 200) { + cookieIfr = JSON.parse(this.responseText).dfpreferrer; + } + }; + xhttp.withCredentials = true; + xhttp.open('GET', '//dfp.gazeta.pl/c.servlet?cn=dfpreferrer', false); + try { + xhttp.send(); + } catch (ex) { + console.info('Error xhttp DFP'); + } +} + +function _setCookieDFP(name) { + var xhttp = new XMLHttpRequest(); + xhttp.withCredentials = true; + xhttp.open('GET', '//dfp.gazeta.pl/c.servlet?cn=dfpreferrer&cv=' + name, true); + try { + xhttp.send(); + } catch (ex) { + console.info('Error xhttp DFP'); + } +} + +function _getCookieValueDFP(a) { + var b = document.cookie.match('(^|;)\\s*' + a + '\\s*=\\s*([^;]+)'); + return b ? b.pop() : ''; +} + + + + +// 20180410 +var hashArrDFP = ['BoxBizCzol1','BoxBizImg1','BoxBizImg2','BoxBizImg3','BoxBizLink','BoxBizLinkImg','BoxBizMT','BoxBizNav','BoxGWImg','BoxHor','BoxMTPromoImg','BoxMTPromo','BoxKultIko','BoxKultImg1','BoxKultImg2','BoxKultImg3','BoxKultLink','BoxKultLinkImg','BoxKultNav','BoxKultNavLink','BoxLoBbImg1','BoxLoBbImg2','BoxLoBbImg3','BoxLoBbImg4','BoxLoBbLink','BoxLoBbMT','BoxLoBbNavLink','BoxLoBiImg1','BoxLoBiImg2','BoxLoBiImg3','BoxLoBiImg4','BoxLoBiLink','BoxLoBiMT','BoxLoByImg1','BoxLoByImg2','BoxLoByImg3','BoxLoByImg4','BoxLoByLink','BoxLoByMT','BoxLoByNavLink','BoxLoCzImg1','BoxLoCzImg2','BoxLoCzImg3','BoxLoCzImg4','BoxLoCzNav','BoxLoCzLink','BoxLoCzMT','BoxLoCzNavLink','BoxLoGLImg1','BoxLoGLImg2','BoxLoGLImg3','BoxLoGLImg4','BoxLoGLLink','BoxLoGLMT','BoxLoGLNavLink','BoxLoGwImg1','BoxLoGwImg2','BoxLoGwImg3','BoxLoGwImg4','BoxLoGwLink','BoxLoGwMT','BoxLoKaImg1','BoxLoKaImg2','BoxLoKaImg3','BoxLoKaImg4','BoxLoKaLink','BoxLoKaMT','BoxLoKaNavLink','BoxLoKiImg1','BoxLoKiImg2','BoxLoKiImg3','BoxLoKiImg4','BoxLoKiLink','BoxLoKiMT','BoxLoKiNavLink','BoxLoKrImg1','BoxLoKrImg2','BoxLoKrImg3','BoxLoKrImg4','BoxLoKrLink','BoxLoKrMT','BoxLoKrNavLink','BoxLoLoImg1','BoxLoLoImg2','BoxLoLoImg3','BoxLoLoImg4','BoxLoLoLink','BoxLoLoMT','BoxLoLoNavLink','BoxLoLuImg1','BoxLoLuImg2','BoxLoLuImg3','BoxLoLuImg4','BoxLoLuLink','BoxLoLuMT','BoxLoLuNavLink','BoxLoOlsztynImg1','BoxLoOlsztynImg2','BoxLoOlsztynImg3','BoxLoOlsztynImg4','BoxLoOlsztynLink','BoxLoOlsztynMT','BoxLoOlsztynNavLink','BoxLoOlsztynNav','BoxLoOpImg1','BoxLoOpImg2','BoxLoOpImg3','BoxLoOpImg4','BoxLoOpLink','BoxLoOpMT','BoxLoOpNavLink','BoxLoPlImg1','BoxLoPlImg2','BoxLoPlImg3','BoxLoPlImg4','BoxLoPlLink','BoxLoPlMT','BoxLoPoImg1','BoxLoPoImg2','BoxLoPoImg3','BoxLoPoImg4','BoxLoPoLink','BoxLoPoMT','BoxLoPoNavLink','BoxLoRaImg1','BoxLoRaImg2','BoxLoRaImg3','BoxLoRaImg4','BoxLoRaLink','BoxLoRaMT','BoxLoRaNavLink','BoxLoRzImg2','BoxLoRzImg3','BoxLoRzImg4','BoxLoRzLink','BoxLoRzNavLink','BoxLoSoImg1','BoxLoSoImg2','BoxLoSoImg3','BoxLoSoImg4','BoxLoSoLink','BoxLoSoMT','BoxLoSoNavLink','BoxLoSzImg1','BoxLoSzImg2','BoxLoSzImg3','BoxLoSzImg4','BoxLoSzLink','BoxLoSzMT','BoxLoSzNavLink','BoxLoTrImg1','BoxLoTrImg2','BoxLoTrImg3','BoxLoTrImg4','BoxLoTrLink','BoxLoTrMT','BoxLoTrNavLink','BoxLoTrNav','BoxLoWaImg1','BoxLoWaImg2','BoxLoWaImg3','BoxLoWaImg4','BoxLoWaLink','BoxLoWaMT','BoxLoWaNav','BoxLoWrImg1','BoxLoWrImg2','BoxLoWrImg3','BoxLoWrImg4','BoxLoWrLink','BoxLoWrMT','BoxLoWrNavLink','BoxLoZgImg1','BoxLoZgImg2','BoxLoZgImg3','BoxLoZgImg4','BoxLoZgLink','BoxLoZgMT','BoxLoZgNavLink','BoxLSCzol1','BoxLSCzol2','BoxMotMT','BoxMotImg1','BoxMotImg2','BoxMotImg3','BoxMotImg4','BoxMotLink','BoxMotLinkImg','BoxMotNav','BoxLSFCZOL1','BoxLSFImg1','BoxLSFImg2','BoxLSFImg3','BoxLSFImg4','BoxLSFLink','BoxLSFMT','BoxLSImg1','BoxLSImg2','BoxLSLink','BoxLSLinkImg','BoxLSMT','BoxLSNav','ECON','BoxNewestNav','BoxNewsImg1','BoxNewsImg2','BoxNewsImg3','BoxNewsImg4','BoxNewsLink','BoxNewsLinkImg','BoxNewsLinkMore','BoxNewsMT','BoxNewsNav','BoxNewsNavLink','BoxNPrzeCzol1','BoxNPrzeCzol10','BoxNPrzeCzol11','BoxNPrzeCzol12','BoxNPrzeCzol13','BoxNPrzeCzol14','BoxNPrzeCzol15','BoxNPrzeCzol16','BoxNPrzeCzol17','BoxNPrzeCzol18','BoxNPrzeCzol19','BoxNPrzeCzol2','BoxNPrzeCzol20','BoxNPrzeCzol21','BoxNPrzeCzol22','BoxNPrzeCzol23','BoxNPrzeCzol24','BoxNPrzeCzol25','BoxNPrzeCzol26','BoxNPrzeCzol27','BoxNPrzeCzol3','BoxNPrzeCzol4','BoxNPrzeCzol5','BoxNPrzeCzol6','BoxNPrzeCzol7','BoxNPrzeCzol8','BoxNPrzeCzol9','BoxOpCzol1','BoxOpCzol2','BoxOpCzol3','BoxOpImg1','BoxOpImg2','BoxOpImg3','BoxOpImg4','BoxOpImg5','BoxOpImg6','BoxOpImg7','BoxOpImg8','BoxOpImg9','BoxOpImg10','BoxOpImg11','BoxOpLink','BoxOpMT','BoxPloCzol1','BoxPloImg1','BoxPloImg2','BoxPloImg3','BoxPloLink','BoxPloLinkImg','BoxPloMT','BoxPloNavLink','BoxRozCzol1','BoxRozImg1','BoxRozImg2','BoxRozImg3','BoxRozImg4','BoxRozMT','BoxRozNavLink','BoxSportImg1','BoxSportImg2','BoxSportImg3','BoxSportLink','BoxSportLinkImg','BoxSportMT','BoxSportNav','BoxWyboImg1','BoxWyboImg2','BoxWyboImg3','BoxWyboImg4','BoxWyboLink','BoxWyboLinkImg','BoxWyboMT','BoxWyboNav','NavMoreSeo','PublioBoxEcom1','PublioBoxEcom2','PublioBoxEcom3','PublioBoxEcom4','NavLinks','NavLinksSm','NavIco','NavIcoSm','BoxOpMT1','BoxOpMT2','BoxOpMT3','BoxOpMT4','BoxPogTer','BoxPogNast','BoxPogJut','BoxPog7','BoxPogMi','BoxSportImg4','BoxSportCzol1','BoxSportLinkMore','BoxBizImg4','BoxBizLinkMore','BoxLSImg3','BoxLSImg4','BoxLSLinkMore','BoxPloNav','BoxPloLinkMore','BoxLSFCzol1','BoxLSFNav','BoxLSFLinkMore','BoxKultLinkMore','BoxRozNav','BoxRozLink','BoxRozLinkMore','BoxWyboNavLink','BoxWyboLinkMore','BoxLoWaNavLink','BoxLoWaLinkMore','BoxLoKrNav','BoxLoKrLinkMore','BoxLoKiNav','BoxLoKiLinkMore','BoxLoBiNav','BoxLoBiNavLink','BoxLoBiLinkMore','BoxLoBbNav','BoxLoBbLinkMore','BoxLoByNav','BoxLoByLinkMore','BoxLoGlMT','BoxLoGlImg1','BoxLoGlImg2','BoxLoGlImg3','BoxLoGlImg4','BoxLoGlLink','BoxLoGlNav','BoxLoGlNavLink','BoxLoGlLinkMore','BoxLoTrLinkMore','BoxLoGwNav','BoxLoGwNavLink','BoxLoGwLinkMore','BoxLoKaNav','BoxLoKaLinkMore','BoxLoLuNav','BoxLoLuLinkMore','BoxLoLoNav','BoxLoLoLinkMore','BoxLoOlMT','BoxLoOlImg1','BoxLoOlImg2','BoxLoOlImg3','BoxLoOlImg4','BoxLoOlLink','BoxLoOlNav','BoxLoOlNavLink','BoxLoOlLinkMore','BoxLoOpNav','BoxLoOpLinkMore','BoxLoPlNav','BoxLoPlNavLink','BoxLoPlLinkMore','BoxLoPoNav','BoxLoPoLinkMore','BoxLoCzLinkMore','BoxLoRaNav','BoxLoRaLinkMore','BoxLoRzMT','BoxLoRzImg1','BoxLoRzNav','BoxLoRzLinkMore','BoxLoSoNav','BoxLoSoLinkMore','BoxLoSzNav','BoxLoSzLinkMore','BoxLoToMT','BoxLoToImg1','BoxLoToImg2','BoxLoToImg3','BoxLoToImg4','BoxLoToLink','BoxLoToNav','BoxLoToNavLink','BoxLoToLinkMore','BoxLoWrNav','BoxLoWrLinkMore','BoxLoZgNav','BoxLoZgLinkMore','BoxLoCpMT','BoxLoCpImg1','BoxLoCpImg2','BoxLoCpImg3','BoxLoCpImg4','BoxLoCpLink','BoxLoCpNav','BoxLoCpNavLink','BoxLoCpLinkMore','BoxC2COp1','BoxC2COp2','BoxC2COp10','BoxC2COp3','BoxC2COp4','BoxC2COp5','BoxC2COp6','BoxC2COp7','BoxC2COp8','BoxC2COp9','BoxC2CLS1','BoxC2CLS10','BoxC2CLS2','BoxC2CLS3','BoxC2CLS4','BoxC2CLS5','BoxC2CLS6','BoxC2CLS7','BoxC2CLS8','BoxC2CLS9','BoxC2CMot1','BoxC2CMot10','BoxC2CMot2','BoxC2CMot3','BoxC2CMot4','BoxC2CMot5','BoxC2CMot6','BoxC2CMot7','BoxC2CMot8','BoxC2CMot9','BoxC2CPlo1','BoxC2CPlo10','BoxC2CPlo2','BoxC2CPlo3','BoxC2CPlo4','BoxC2CPlo5','BoxC2CPlo6','BoxC2CPlo7','BoxC2CPlo8','BoxC2CPlo9','BoxC2CSport1','BoxC2CSport10','BoxC2CSport2','BoxC2CSport3','BoxC2CSport4','BoxC2CSport5','BoxC2CSport6','BoxC2CSport7','BoxC2CSport8','BoxC2CSport9','BoxWeSl_1_1','BoxWeSl_1_2','BoxWeSl_2_1','BoxWeSl_2_2','BoxWeSl_3_1','BoxWeSl_3_2','BoxWeSl_4_1','BoxWeSl_4_2','BoxWeSl_5_1','BoxWeSl_5_2','BoxWeFr','BoxLoBbLinkImg','BoxLoBiLinkImg','BoxLoByLinkImg','BoxLoCzLinkImg','BoxLoGLLinkImg','BoxLoGwLinkImg','BoxLoKaLinkImg','BoxLoKiLinkImg','BoxLoKrLinkImg','BoxLoLoLinkImg','BoxLoLuLinkImg','BoxLoOlsztynLinkImg','BoxLoOpLinkImg','BoxLoPlLinkImg','BoxLoPoLinkImg','BoxLoRaLinkImg','BoxLoRzLinkImg','BoxLoSoLinkImg','BoxLoSzLinkImg','BoxLoTrLinkImg','BoxLoWaLinkImg','BoxLoWrLinkImg','BoxLoZgLinkImg','BoxLoGlLinkImg','BoxLoOlLinkImg','BoxLoToLinkImg','BoxLoCpLinkImg','BoxBizCz','BoxBizImg','BoxDesCz','BoxDesImg','BoxDesNav','BoxDWCz','BoxDWImg','BoxDWNav','BoxLSECom','BoxFiBImg','BoxFiBNav','FiBImg','FiBNav','BoxGWNav','BoxHorNav','BoxIkoBook','BoxIkoGie','BoxIkoGry','BoxIkoHor','BoxIkoKina','BoxIkoLotto','BoxIkoPog','BoxIkoTV','BoxKultImg','BoxLSCz','BoxLSImg','BoxLokKrajImg','BoxLokKrajLink','BoxLokKrajLinkImg','BoxLokBialImg','BoxLokBialLink','BoxLokBialNav','BoxLokBialLinkImg','BoxLokBieImg','BoxLokBieLink','BoxLokBieLinkImg','BoxLokBieNav','BoxLokBydImg','BoxLokBydLink','BoxLokBydLinkImg','BoxLokBydNav','BoxLokCzeImg','BoxLokCzeLink','BoxLokCzeLinkImg','BoxLokCzeNav','BoxLokGlwImg','BoxLokGlwLink','BoxLokGlwLinkImg','BoxLokGorzImg','BoxLokGorzLink','BoxLokGorzLinkImg','BoxLokKatImg','BoxLokKatLink','BoxLokKatLinkImg','BoxLokKatNav','BoxLokKielImg','BoxLokKielLink','BoxLokKielLinkImg','BoxLokKielNav','BoxLokKrakImg','BoxLokKrakLink','BoxLokKrakLinkImg','BoxLokKrakNav','BoxLokLubImg','BoxLokLubLink','BoxLokLubLinkImg','BoxLokLubNav','BoxLokLodzImg','BoxLokLodzLink','BoxLokLodzLinkImg','BoxLokLodzNav','BoxLokOlsImg','BoxLokOlsLink','BoxLokOlsNav','BoxLokOlsLinkImg','BoxLokOpoImg','BoxLokOpoLink','BoxLokOpoLinkImg','BoxLokOpoNav','BoxLokPloImg','BoxLokPloLink','BoxLokPloNav','BoxLokPloLinkImg','BoxLokPozImg','BoxLokPozLink','BoxLokPozNav','BoxLokPozLinkImg','BoxLokRadImg','BoxLokRadLink','BoxLokRadNav','BoxLokRadLinkImg','BoxLokRzeImg','BoxLokRzeLink','BoxLokRzeLinkImg','BoxLokRzeNav','BoxLokSosImg','BoxLokSosLink','BoxLokSosLinkImg','BoxLokSosNav','BoxLokSznImg','BoxLokSznLink','BoxLokSznLinkImg','BoxLokSznNav','BoxLokTorImg','BoxLokTorLink','BoxLokTorLinkImg','BoxLokTorNav','BoxLokTrojImg','BoxLokTrojLink','BoxLokTrojLinkImg','BoxLokTrojNav','BoxLokWawImg','BoxLokWawNav','BoxLokWawLink','BoxLokWawLinkImg','BoxLokWrocImg','BoxLokWrocLink','BoxLokWrocLinkImg','BoxLokWrocNav','BoxLokZielImg','BoxLokZielLink','BoxLokZielLinkImg','BoxLokZielNav','MegaMT','BoxMU2Img','ModaUroda2Img','BoxMUCz','BoxMUImg','BoxMotoImg','BoxMotoNav','MotoImg','MTstream','mBoxNajcz','BoxNiePrzeg','NiePrzeg','BoxOferty','BoxNextImg','Czolka3Img','MT','MT2','Prze','BoxPloCz2','BoxPloCz3','BoxPloPrze','BoxTripImg','BoxTrvCz','BoxTrvImg','BoxTrvNav','PodrozeCz','PodrozeImg','TRPog','BoxPubImg','BoxRekrCz','BoxRekrImg','BoxRekrNav','BoxSpecImg','BoxSpecLink','BoxSpecialNav','BoxSpecial','BoxSportCz','BoxSportImg','BoxSportPrze','Weekend','TRwknd','BoxNewsImg','BoxVidImg','BoxVidLifeImg','BoxVODImg','BoxWN','gospoIco','Img','Ogl_all','Czolka3Img2','Prze2','HatNav','NavLinksCz','HatNavCz','NavMore','TROgl','BoxPubNav','A_BoxBizCzol1','A_BoxBizImg1','A_BoxBizImg2','A_BoxBizImg3','A_BoxBizLink','A_BoxBizLinkImg','A_BoxBizMT','A_BoxBizNav','A_BoxGWImg','A_BoxHor','A_BoxMTPromoImg','A_BoxMTPromo','A_BoxKultIko','A_BoxKultImg1','A_BoxKultImg2','A_BoxKultImg3','A_BoxKultLink','A_BoxKultLinkImg','A_BoxKultNav','A_BoxKultNavLink','A_BoxLoBbImg1','A_BoxLoBbImg2','A_BoxLoBbImg3','A_BoxLoBbImg4','A_BoxLoBbLink','A_BoxLoBbMT','A_BoxLoBbNavLink','A_BoxLoBiImg1','A_BoxLoBiImg2','A_BoxLoBiImg3','A_BoxLoBiImg4','A_BoxLoBiLink','A_BoxLoBiMT','A_BoxLoByImg1','A_BoxLoByImg2','A_BoxLoByImg3','A_BoxLoByImg4','A_BoxLoByLink','A_BoxLoByMT','A_BoxLoByNavLink','A_BoxLoCzImg1','A_BoxLoCzImg2','A_BoxLoCzImg3','A_BoxLoCzImg4','A_BoxLoCzNav','A_BoxLoCzLink','A_BoxLoCzMT','A_BoxLoCzNavLink','A_BoxLoGLImg1','A_BoxLoGLImg2','A_BoxLoGLImg3','A_BoxLoGLImg4','A_BoxLoGLLink','A_BoxLoGLMT','A_BoxLoGLNavLink','A_BoxLoGwImg1','A_BoxLoGwImg2','A_BoxLoGwImg3','A_BoxLoGwImg4','A_BoxLoGwLink','A_BoxLoGwMT','A_BoxLoKaImg1','A_BoxLoKaImg2','A_BoxLoKaImg3','A_BoxLoKaImg4','A_BoxLoKaLink','A_BoxLoKaMT','A_BoxLoKaNavLink','A_BoxLoKiImg1','A_BoxLoKiImg2','A_BoxLoKiImg3','A_BoxLoKiImg4','A_BoxLoKiLink','A_BoxLoKiMT','A_BoxLoKiNavLink','A_BoxLoKrImg1','A_BoxLoKrImg2','A_BoxLoKrImg3','A_BoxLoKrImg4','A_BoxLoKrLink','A_BoxLoKrMT','A_BoxLoKrNavLink','A_BoxLoLoImg1','A_BoxLoLoImg2','A_BoxLoLoImg3','A_BoxLoLoImg4','A_BoxLoLoLink','A_BoxLoLoMT','A_BoxLoLoNavLink','A_BoxLoLuImg1','A_BoxLoLuImg2','A_BoxLoLuImg3','A_BoxLoLuImg4','A_BoxLoLuLink','A_BoxLoLuMT','A_BoxLoLuNavLink','A_BoxLoOlsztynImg1','A_BoxLoOlsztynImg2','A_BoxLoOlsztynImg3','A_BoxLoOlsztynImg4','A_BoxLoOlsztynLink','A_BoxLoOlsztynMT','A_BoxLoOlsztynNavLink','A_BoxLoOlsztynNav','A_BoxLoOpImg1','A_BoxLoOpImg2','A_BoxLoOpImg3','A_BoxLoOpImg4','A_BoxLoOpLink','A_BoxLoOpMT','A_BoxLoOpNavLink','A_BoxLoPlImg1','A_BoxLoPlImg2','A_BoxLoPlImg3','A_BoxLoPlImg4','A_BoxLoPlLink','A_BoxLoPlMT','A_BoxLoPoImg1','A_BoxLoPoImg2','A_BoxLoPoImg3','A_BoxLoPoImg4','A_BoxLoPoLink','A_BoxLoPoMT','A_BoxLoPoNavLink','A_BoxLoRaImg1','A_BoxLoRaImg2','A_BoxLoRaImg3','A_BoxLoRaImg4','A_BoxLoRaLink','A_BoxLoRaMT','A_BoxLoRaNavLink','A_BoxLoRzImg2','A_BoxLoRzImg3','A_BoxLoRzImg4','A_BoxLoRzLink','A_BoxLoRzNavLink','A_BoxLoSoImg1','A_BoxLoSoImg2','A_BoxLoSoImg3','A_BoxLoSoImg4','A_BoxLoSoLink','A_BoxLoSoMT','A_BoxLoSoNavLink','A_BoxLoSzImg1','A_BoxLoSzImg2','A_BoxLoSzImg3','A_BoxLoSzImg4','A_BoxLoSzLink','A_BoxLoSzMT','A_BoxLoSzNavLink','A_BoxLoTrImg1','A_BoxLoTrImg2','A_BoxLoTrImg3','A_BoxLoTrImg4','A_BoxLoTrLink','A_BoxLoTrMT','A_BoxLoTrNavLink','A_BoxLoTrNav','A_BoxLoWaImg1','A_BoxLoWaImg2','A_BoxLoWaImg3','A_BoxLoWaImg4','A_BoxLoWaLink','A_BoxLoWaMT','A_BoxLoWaNav','A_BoxLoWrImg1','A_BoxLoWrImg2','A_BoxLoWrImg3','A_BoxLoWrImg4','A_BoxLoWrLink','A_BoxLoWrMT','A_BoxLoWrNavLink','A_BoxLoZgImg1','A_BoxLoZgImg2','A_BoxLoZgImg3','A_BoxLoZgImg4','A_BoxLoZgLink','A_BoxLoZgMT','A_BoxLoZgNavLink','A_BoxLSCzol1','A_BoxLSCzol2','A_BoxMotMT','A_BoxMotImg1','A_BoxMotImg2','A_BoxMotImg3','A_BoxMotImg4','A_BoxMotLink','A_BoxMotLinkImg','A_BoxMotNav','A_BoxLSFCZOL1','A_BoxLSFImg1','A_BoxLSFImg2','A_BoxLSFImg3','A_BoxLSFImg4','A_BoxLSFLink','A_BoxLSFMT','A_BoxLSImg1','A_BoxLSImg2','A_BoxLSLink','A_BoxLSLinkImg','A_BoxLSMT','A_BoxLSNav','A_ECON','A_BoxNewestNav','A_BoxNewsImg1','A_BoxNewsImg2','A_BoxNewsImg3','A_BoxNewsImg4','A_BoxNewsLink','A_BoxNewsLinkImg','A_BoxNewsLinkMore','A_BoxNewsMT','A_BoxNewsNav','A_BoxNewsNavLink','A_BoxNPrzeCzol1','A_BoxNPrzeCzol10','A_BoxNPrzeCzol11','A_BoxNPrzeCzol12','A_BoxNPrzeCzol13','A_BoxNPrzeCzol14','A_BoxNPrzeCzol15','A_BoxNPrzeCzol16','A_BoxNPrzeCzol17','A_BoxNPrzeCzol18','A_BoxNPrzeCzol19','A_BoxNPrzeCzol2','A_BoxNPrzeCzol20','A_BoxNPrzeCzol21','A_BoxNPrzeCzol22','A_BoxNPrzeCzol23','A_BoxNPrzeCzol24','A_BoxNPrzeCzol25','A_BoxNPrzeCzol26','A_BoxNPrzeCzol27','A_BoxNPrzeCzol3','A_BoxNPrzeCzol4','A_BoxNPrzeCzol5','A_BoxNPrzeCzol6','A_BoxNPrzeCzol7','A_BoxNPrzeCzol8','A_BoxNPrzeCzol9','A_BoxOpCzol1','A_BoxOpCzol2','A_BoxOpCzol3','A_BoxOpImg1','A_BoxOpImg2','A_BoxOpImg3','A_BoxOpImg4','A_BoxOpImg5','A_BoxOpImg6','A_BoxOpImg7','A_BoxOpImg8','A_BoxOpImg9','A_BoxOpImg10','A_BoxOpImg11','A_BoxOpLink','A_BoxOpMT','A_BoxPloCzol1','A_BoxPloImg1','A_BoxPloImg2','A_BoxPloImg3','A_BoxPloLink','A_BoxPloLinkImg','A_BoxPloMT','A_BoxPloNavLink','A_BoxRozCzol1','A_BoxRozImg1','A_BoxRozImg2','A_BoxRozImg3','A_BoxRozImg4','A_BoxRozMT','A_BoxRozNavLink','A_BoxSportImg1','A_BoxSportImg2','A_BoxSportImg3','A_BoxSportLink','A_BoxSportLinkImg','A_BoxSportMT','A_BoxSportNav','A_BoxWyboImg1','A_BoxWyboImg2','A_BoxWyboImg3','A_BoxWyboImg4','A_BoxWyboLink','A_BoxWyboLinkImg','A_BoxWyboMT','A_BoxWyboNav','A_NavMoreSeo','A_PublioBoxEcom1','A_PublioBoxEcom2','A_PublioBoxEcom3','A_PublioBoxEcom4','A_NavLinks','A_NavLinksSm','A_NavIco','A_NavIcoSm','A_BoxOpMT1','A_BoxOpMT2','A_BoxOpMT3','A_BoxOpMT4','A_BoxPogTer','A_BoxPogNast','A_BoxPogJut','A_BoxPog7','A_BoxPogMi','A_BoxSportImg4','A_BoxSportCzol1','A_BoxSportLinkMore','A_BoxBizImg4','A_BoxBizLinkMore','A_BoxLSImg3','A_BoxLSImg4','A_BoxLSLinkMore','A_BoxPloNav','A_BoxPloLinkMore','A_BoxLSFCzol1','A_BoxLSFNav','A_BoxLSFLinkMore','A_BoxKultLinkMore','A_BoxRozNav','A_BoxRozLink','A_BoxRozLinkMore','A_BoxWyboNavLink','A_BoxWyboLinkMore','A_BoxLoWaNavLink','A_BoxLoWaLinkMore','A_BoxLoKrNav','A_BoxLoKrLinkMore','A_BoxLoKiNav','A_BoxLoKiLinkMore','A_BoxLoBiNav','A_BoxLoBiNavLink','A_BoxLoBiLinkMore','A_BoxLoBbNav','A_BoxLoBbLinkMore','A_BoxLoByNav','A_BoxLoByLinkMore','A_BoxLoGlMT','A_BoxLoGlImg1','A_BoxLoGlImg2','A_BoxLoGlImg3','A_BoxLoGlImg4','A_BoxLoGlLink','A_BoxLoGlNav','A_BoxLoGlNavLink','A_BoxLoGlLinkMore','A_BoxLoTrLinkMore','A_BoxLoGwNav','A_BoxLoGwNavLink','A_BoxLoGwLinkMore','A_BoxLoKaNav','A_BoxLoKaLinkMore','A_BoxLoLuNav','A_BoxLoLuLinkMore','A_BoxLoLoNav','A_BoxLoLoLinkMore','A_BoxLoOlMT','A_BoxLoOlImg1','A_BoxLoOlImg2','A_BoxLoOlImg3','A_BoxLoOlImg4','A_BoxLoOlLink','A_BoxLoOlNav','A_BoxLoOlNavLink','A_BoxLoOlLinkMore','A_BoxLoOpNav','A_BoxLoOpLinkMore','A_BoxLoPlNav','A_BoxLoPlNavLink','A_BoxLoPlLinkMore','A_BoxLoPoNav','A_BoxLoPoLinkMore','A_BoxLoCzLinkMore','A_BoxLoRaNav','A_BoxLoRaLinkMore','A_BoxLoRzMT','A_BoxLoRzImg1','A_BoxLoRzNav','A_BoxLoRzLinkMore','A_BoxLoSoNav','A_BoxLoSoLinkMore','A_BoxLoSzNav','A_BoxLoSzLinkMore','A_BoxLoToMT','A_BoxLoToImg1','A_BoxLoToImg2','A_BoxLoToImg3','A_BoxLoToImg4','A_BoxLoToLink','A_BoxLoToNav','A_BoxLoToNavLink','A_BoxLoToLinkMore','A_BoxLoWrNav','A_BoxLoWrLinkMore','A_BoxLoZgNav','A_BoxLoZgLinkMore','A_BoxLoCpMT','A_BoxLoCpImg1','A_BoxLoCpImg2','A_BoxLoCpImg3','A_BoxLoCpImg4','A_BoxLoCpLink','A_BoxLoCpNav','A_BoxLoCpNavLink','A_BoxLoCpLinkMore','A_BoxC2COp1','A_BoxC2COp2','A_BoxC2COp10','A_BoxC2COp3','A_BoxC2COp4','A_BoxC2COp5','A_BoxC2COp6','A_BoxC2COp7','A_BoxC2COp8','A_BoxC2COp9','A_BoxC2CLS1','A_BoxC2CLS10','A_BoxC2CLS2','A_BoxC2CLS3','A_BoxC2CLS4','A_BoxC2CLS5','A_BoxC2CLS6','A_BoxC2CLS7','A_BoxC2CLS8','A_BoxC2CLS9','A_BoxC2CMot1','A_BoxC2CMot10','A_BoxC2CMot2','A_BoxC2CMot3','A_BoxC2CMot4','A_BoxC2CMot5','A_BoxC2CMot6','A_BoxC2CMot7','A_BoxC2CMot8','A_BoxC2CMot9','A_BoxC2CPlo1','A_BoxC2CPlo10','A_BoxC2CPlo2','A_BoxC2CPlo3','A_BoxC2CPlo4','A_BoxC2CPlo5','A_BoxC2CPlo6','A_BoxC2CPlo7','A_BoxC2CPlo8','A_BoxC2CPlo9','A_BoxC2CSport1','A_BoxC2CSport10','A_BoxC2CSport2','A_BoxC2CSport3','A_BoxC2CSport4','A_BoxC2CSport5','A_BoxC2CSport6','A_BoxC2CSport7','A_BoxC2CSport8','A_BoxC2CSport9','A_BoxWeSl_1_1','A_BoxWeSl_1_2','A_BoxWeSl_2_1','A_BoxWeSl_2_2','A_BoxWeSl_3_1','A_BoxWeSl_3_2','A_BoxWeSl_4_1','A_BoxWeSl_4_2','A_BoxWeSl_5_1','A_BoxWeSl_5_2','A_BoxWeFr','A_BoxLoBbLinkImg','A_BoxLoBiLinkImg','A_BoxLoByLinkImg','A_BoxLoCzLinkImg','A_BoxLoGLLinkImg','A_BoxLoGwLinkImg','A_BoxLoKaLinkImg','A_BoxLoKiLinkImg','A_BoxLoKrLinkImg','A_BoxLoLoLinkImg','A_BoxLoLuLinkImg','A_BoxLoOlsztynLinkImg','A_BoxLoOpLinkImg','A_BoxLoPlLinkImg','A_BoxLoPoLinkImg','A_BoxLoRaLinkImg','A_BoxLoRzLinkImg','A_BoxLoSoLinkImg','A_BoxLoSzLinkImg','A_BoxLoTrLinkImg','A_BoxLoWaLinkImg','A_BoxLoWrLinkImg','A_BoxLoZgLinkImg','A_BoxLoGlLinkImg','A_BoxLoOlLinkImg','A_BoxLoToLinkImg','A_BoxLoCpLinkImg','Z_BoxBizCz','Z_BoxBizImg','Z_BoxBizLink','Z_BoxBizLinkImg','Z_BoxBizNav','Z_BoxDesCz','Z_BoxDesImg','Z_BoxDesNav','Z_BoxDWCz','Z_BoxDWImg','Z_BoxDWNav','Z_BoxLSECom','Z_BoxFiBImg','Z_BoxFiBNav','Z_FiBImg','Z_FiBNav','Z_BoxGWImg','Z_BoxGWNav','Z_BoxHor','Z_BoxHorNav','Z_BoxIkoBook','Z_BoxIkoGie','Z_BoxIkoGry','Z_BoxIkoHor','Z_BoxIkoKina','Z_BoxIkoLotto','Z_BoxIkoPog','Z_BoxIkoTV','Z_BoxKultImg','Z_BoxKultNav','Z_BoxLSCz','Z_BoxLSImg','Z_BoxLSLink','Z_BoxLSLinkImg','Z_BoxLSNav','Z_BoxLokKrajImg','Z_BoxLokKrajLink','Z_BoxLokKrajLinkImg','Z_BoxLokBialImg','Z_BoxLokBialLink','Z_BoxLokBialNav','Z_BoxLokBialLinkImg','Z_BoxLokBieImg','Z_BoxLokBieLink','Z_BoxLokBieLinkImg','Z_BoxLokBieNav','Z_BoxLokBydImg','Z_BoxLokBydLink','Z_BoxLokBydLinkImg','Z_BoxLokBydNav','Z_BoxLokCzeImg','Z_BoxLokCzeLink','Z_BoxLokCzeLinkImg','Z_BoxLokCzeNav','Z_BoxLokGlwImg','Z_BoxLokGlwLink','Z_BoxLokGlwLinkImg','Z_BoxLokGorzImg','Z_BoxLokGorzLink','Z_BoxLokGorzLinkImg','Z_BoxLokKatImg','Z_BoxLokKatLink','Z_BoxLokKatLinkImg','Z_BoxLokKatNav','Z_BoxLokKielImg','Z_BoxLokKielLink','Z_BoxLokKielLinkImg','Z_BoxLokKielNav','Z_BoxLokKrakImg','Z_BoxLokKrakLink','Z_BoxLokKrakLinkImg','Z_BoxLokKrakNav','Z_BoxLokLubImg','Z_BoxLokLubLink','Z_BoxLokLubLinkImg','Z_BoxLokLubNav','Z_BoxLokLodzImg','Z_BoxLokLodzLink','Z_BoxLokLodzLinkImg','Z_BoxLokLodzNav','Z_BoxLokOlsImg','Z_BoxLokOlsLink','Z_BoxLokOlsNav','Z_BoxLokOlsLinkImg','Z_BoxLokOpoImg','Z_BoxLokOpoLink','Z_BoxLokOpoLinkImg','Z_BoxLokOpoNav','Z_BoxLokPloImg','Z_BoxLokPloLink','Z_BoxLokPloNav','Z_BoxLokPloLinkImg','Z_BoxLokPozImg','Z_BoxLokPozLink','Z_BoxLokPozNav','Z_BoxLokPozLinkImg','Z_BoxLokRadImg','Z_BoxLokRadLink','Z_BoxLokRadNav','Z_BoxLokRadLinkImg','Z_BoxLokRzeImg','Z_BoxLokRzeLink','Z_BoxLokRzeLinkImg','Z_BoxLokRzeNav','Z_BoxLokSosImg','Z_BoxLokSosLink','Z_BoxLokSosLinkImg','Z_BoxLokSosNav','Z_BoxLokSznImg','Z_BoxLokSznLink','Z_BoxLokSznLinkImg','Z_BoxLokSznNav','Z_BoxLokTorImg','Z_BoxLokTorLink','Z_BoxLokTorLinkImg','Z_BoxLokTorNav','Z_BoxLokTrojImg','Z_BoxLokTrojLink','Z_BoxLokTrojLinkImg','Z_BoxLokTrojNav','Z_BoxLokWawImg','Z_BoxLokWawNav','Z_BoxLokWawLink','Z_BoxLokWawLinkImg','Z_BoxLokWrocImg','Z_BoxLokWrocLink','Z_BoxLokWrocLinkImg','Z_BoxLokWrocNav','Z_BoxLokZielImg','Z_BoxLokZielLink','Z_BoxLokZielLinkImg','Z_BoxLokZielNav','Z_MegaMT','Z_BoxMU2Img','Z_ModaUroda2Img','Z_BoxMUCz','Z_BoxMUImg','Z_BoxMotoImg','Z_BoxMotoNav','Z_MotoImg','Z_BoxMTPromoImg','Z_BoxMTPromo','Z_MTstream','Z_mBoxNajcz','Z_BoxNiePrzeg','Z_NiePrzeg','Z_BoxOferty','Z_BoxNextImg','Z_Czolka3Img','Z_MT','Z_MT2','Z_Prze','Z_BoxPloCz2','Z_BoxPloCz3','Z_BoxPloMT','Z_BoxPloPrze','Z_BoxPloNav','Z_BoxSportNav','Z_BoxTripImg','Z_BoxTrvCz','Z_BoxTrvImg','Z_BoxTrvNav','Z_PodrozeCz','Z_PodrozeImg','Z_BoxPogJut','Z_BoxPogMi','Z_BoxPogTer','Z_TRPog','Z_BoxPubImg','Z_BoxRekrCz','Z_BoxRekrImg','Z_BoxRekrNav','Z_BoxSpecImg','Z_BoxSpecLink','Z_BoxSpecialNav','Z_BoxSpecial','Z_BoxSportCz','Z_BoxSportImg','Z_BoxSportLink','Z_BoxSportLinkImg','Z_BoxSportPrze','Z_Weekend','Z_TRwknd','Z_BoxNewsImg','Z_BoxNewsNav','Z_BoxNewsLink','Z_BoxNewsLinkImg','Z_BoxVidImg','Z_BoxVidLifeImg','Z_BoxVODImg','Z_BoxWN','Z_gospoIco','Z_Img','Z_Ogl_all','Z_Czolka3Img2','Z_Prze2','Z_NavLinks','Z_HatNav','Z_NavLinksCz','Z_HatNavCz','Z_NavIco','Z_NavMore','Z_TROgl','Z_BoxPubNav','BoxMTPPush','Z_BoxMTPPush','A_BoxMTPPush','BoxMTPAuto','A_BoxMTPAuto','Z_BoxMTPAuto']; + + + + +if (( !document.referrer && (window.location.host === 'www.gazeta.pl' || window.location.host === 'm.gazeta.pl') && + (window.location.href.indexOf('0,0.html?p=') !== -1 || window.location.href.indexOf('0,0.html?promocja=') !== -1)) || + ((document.referrer.indexOf('www.gazeta.pl') !== -1 || document.referrer.indexOf('m.gazeta.pl') !== -1) && + (document.referrer.indexOf('0,0.html?p=') !== -1 || document.referrer.indexOf('0,0.html?promocja=') !== -1))) { + + var hashTag = ''; + if (window.location.hash) { + var + hash = window.location.hash, + n = hash.indexOf('&'); + hash = hash.substring(0, n != -1 ? n : hash.length); + var getHash = hash.split('#').pop(); + if (hashArrDFP.indexOf(getHash) !== -1) { hashTag = '_' + getHash; } + } + + googletag.pubads().setTargeting('src','HPGAZETA-DHP' + hashTag); + _setCookieDFP('HPGAZETA-DHP' + hashTag); + if (window.adviewDFP && adviewDFP.DFPTargeting) adviewDFP.DFPTargeting.push("src|HPGAZETA-DHP" + hashTag); + +} else if ( document.referrer.split("?")[0].indexOf('facebook.com') !== -1 || + ( !document.referrer && document.location.href.indexOf('utm_source=facebook.com') !== -1 ) ) { + + if (window.location.href.indexOf('utm_content') !== -1 || document.referrer.indexOf('utm_content') !== -1) { + + var paramsObject = function ( name, url ) { + if (!url) url = location.href; + name = name.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]"); + var regexS = "[\\?&]"+name+"=([^&#]*)"; + var regex = new RegExp( regexS ); + var results = regex.exec( url ); + return results == null ? null : results[1]; + }, + checkHrefDFP = paramsObject('utm_content', window.location.href), + checkReferrerDFP = paramsObject('utm_content', document.referrer), + newValueDFP = checkHrefDFP || checkReferrerDFP; + + googletag.pubads().setTargeting('src', 'FACEBOOK_CPC_' + newValueDFP); + _setCookieDFP('FACEBOOK_CPC_' + newValueDFP); + if (window.adviewDFP && adviewDFP.DFPTargeting) adviewDFP.DFPTargeting.push("src|FACEBOOK_CPC_" + newValueDFP); + + } else { + googletag.pubads().setTargeting('src','FACEBOOK'); + _setCookieDFP('FACEBOOK'); + if (window.adviewDFP && adviewDFP.DFPTargeting) adviewDFP.DFPTargeting.push("src|FACEBOOK"); + } + + +} else if ( ( document.location.href.indexOf('utm_medium=cpc') !== -1 && document.location.href.indexOf('utm_source=facebook.com') === -1) || + (document.location.href.indexOf('adw=1') !== -1 || document.location.href.indexOf('gclid=') !== -1) || + (document.referrer.indexOf('googleads.g.doubleclick.net') !== -1 || document.referrer.indexOf('googleadservices.com') !== -1) ) { + + googletag.pubads().setTargeting('src','CPC'); + _setCookieDFP('CPC'); + if (window.adviewDFP && adviewDFP.DFPTargeting) adviewDFP.DFPTargeting.push("src|CPC"); + +} else if( (document.location.href.indexOf('www.gazeta.pl') !== -1 || document.location.href.indexOf('m.gazeta.pl') !== -1) || + (document.referrer.indexOf('www.gazeta.pl') !== -1 || document.referrer.indexOf('m.gazeta.pl') !== -1) ) { + + var hashTag = ''; + if (window.location.hash) { + var + hash = window.location.hash, + n = hash.indexOf('&'); + hash = hash.substring(0, n != -1 ? n : hash.length); + var getHash = hash.split('#').pop(); + if (hashArrDFP.indexOf(getHash) !== -1) { hashTag = '_' + getHash; } + } + + googletag.pubads().setTargeting('src','HPGAZETA' + hashTag); + _setCookieDFP('HPGAZETA' + hashTag); + if (window.adviewDFP && adviewDFP.DFPTargeting) adviewDFP.DFPTargeting.push("src|HPGAZETA" + hashTag); + +} else if (!document.referrer) { + + googletag.pubads().setTargeting('src','DIRECT'); + _setCookieDFP('DIRECT'); + if (window.adviewDFP && adviewDFP.DFPTargeting) adviewDFP.DFPTargeting.push("src|DIRECT"); + +} else if ( (document.referrer.indexOf('szukaj.gazeta.pl') === -1) && + (document.referrer.indexOf('?q=') !== -1 || document.referrer.indexOf('&q=') !== -1 || document.referrer.indexOf('?query=') !== -1 || document.referrer.indexOf('search.yahoo.com') !== -1 || document.referrer.indexOf('&query=') !== -1 || document.referrer.indexOf('?szukaj=') !== -1 || document.referrer.indexOf('&szukaj') !== -1 || (document.referrer.indexOf('//www.google.') !== -1 && document.referrer.indexOf('logout') === -1)) && + document.location.href.indexOf('0,0.html') !== -1 || window.location.href.slice(-3) === '.pl' || window.location.href.slice(-4) === '.biz' || window.location.href.slice(-3) === '.tv' || window.location.href.slice(-8) === '.pl/html') { + + googletag.pubads().setTargeting('src','DIRECT-SEO'); + _setCookieDFP('DIRECT-SEO'); + if (window.adviewDFP && adviewDFP.DFPTargeting) adviewDFP.DFPTargeting.push("src|DIRECT-SEO"); + +} else if ( document.referrer.indexOf('szukaj.gazeta.pl') === -1 && + (document.referrer.indexOf('?q=') !== -1 || document.referrer.indexOf('&q=') !== -1 || document.referrer.indexOf('?query=') !== -1 || document.referrer.indexOf('search.yahoo.com') !== -1 || document.referrer.indexOf('&query=') !== -1 || document.referrer.indexOf('?szukaj=') !== -1 || document.referrer.indexOf('&szukaj') !== -1 || (document.referrer.indexOf('//www.google.') !== -1 && document.referrer.indexOf('logout') === -1)) ) { + + googletag.pubads().setTargeting('src','SEO'); + _setCookieDFP('SEO'); + if (window.adviewDFP && adviewDFP.DFPTargeting) adviewDFP.DFPTargeting.push("src|SEO"); + +} else if (cookieIfr) { + + googletag.pubads().setTargeting('src', cookieIfr); + if (window.adviewDFP && adviewDFP.DFPTargeting) adviewDFP.DFPTargeting.push("src|" + cookieIfr); + +} + + +if ((window.location.host == "www.gazeta.pl") || (window.location.host == "m.gazeta.pl")) { + if (window.location.href.indexOf('pushpushgo')!== -1 && window.location.href.indexOf('mtp=')!== -1) { + googletag.pubads().setTargeting('test','MTP_PUSH'); + if (window.adviewDFP && adviewDFP.DFPTargeting) adviewDFP.DFPTargeting.push("test|MTP_PUSH"); + } else if ((window.location.href.indexOf('utm_source=facebook')!== -1 && window.location.href.indexOf('mtp=')!== -1) || window.location.href.indexOf('boxmtpautosp=')!== -1) { + googletag.pubads().setTargeting('test','MTP_FBCPC'); + if (window.adviewDFP && adviewDFP.DFPTargeting) adviewDFP.DFPTargeting.push("test|MTP_FBCPC"); + } else if (window.location.href.indexOf('boxmtpauto=')!== -1) { + googletag.pubads().setTargeting('test','MTP_FBORG'); + if (window.adviewDFP && adviewDFP.DFPTargeting) adviewDFP.DFPTargeting.push("test|MTP_FBORG"); + } else if ((window.location.href.indexOf('?p=')!== -1 || window.location.href.indexOf('?promocja=')!== -1) && document.referrer == "") { + googletag.pubads().setTargeting('test','MTP_DHP'); + if (window.adviewDFP && adviewDFP.DFPTargeting) adviewDFP.DFPTargeting.push("test|MTP_DHP"); + } else if (window.location.href.indexOf('utm_campaign=amtp')!== -1) { + googletag.pubads().setTargeting('test','MTP_AMTP'); + if (window.adviewDFP && adviewDFP.DFPTargeting) adviewDFP.DFPTargeting.push("test|MTP_AMTP4"); + } else if (window.location.href.indexOf('?404')!== -1) { + googletag.pubads().setTargeting('test','MTP_404'); + if (window.adviewDFP && adviewDFP.DFPTargeting) adviewDFP.DFPTargeting.push("test|MTP_404"); + }else { + googletag.pubads().setTargeting('test','MTP_NULL'); + if (window.adviewDFP && adviewDFP.DFPTargeting) adviewDFP.DFPTargeting.push("test|MTP_NULL"); + } +} + + + + + + + // [HB] Core-prod-3 - AFTER_SLOTS +if(window.yb_seg && yb_seg!='') { + googletag.pubads().setTargeting('yb_seg', yb_seg); + if (window.adviewDFP && adviewDFP.DFPTargeting) adviewDFP.DFPTargeting.push("yb_seg|" + yb_seg); +} +if(window.pbjs_yb_hb) { + googletag.pubads().getSlots().forEach(function(slot){ + slot.setTargeting('yb_hb', String(yb_hb)); + }); + if (window.adviewDFP && adviewDFP.DFPTargeting) adviewDFP.DFPTargeting.push("yb_hb|" + String(yb_hb)); +} +if(window.pbjs) pbjs.que.push(function() { + pbjs.setTargetingForGPTAsync(); +}); + + + + + + + + var yb_ff = String(Math.round(Math.random())); +googletag.pubads().setTargeting('yb_ff', yb_ff); +var _yt=new Date(),yb_th=_yt.getUTCHours()-8,yb_tm=_yt.getUTCMinutes(),yb_wd=_yt.getUTCDay(); +if(yb_th<0){yb_th=24+yb_th;yb_wd-=1;};if(yb_wd<0){yb_wd=7+yb_wd}; +googletag.pubads().setTargeting('yb_th', yb_th.toString()); +googletag.pubads().setTargeting('yb_tm', yb_tm.toString()); +googletag.pubads().setTargeting('yb_wd', yb_wd.toString()); + +if (window.adviewDFP && adviewDFP.DFPTargeting) adviewDFP.DFPTargeting.push("yb_th|" + yb_th.toString()); +if (window.adviewDFP && adviewDFP.DFPTargeting) adviewDFP.DFPTargeting.push("yb_tm|" + yb_tm.toString()); +if (window.adviewDFP && adviewDFP.DFPTargeting) adviewDFP.DFPTargeting.push("yb_wd|" + yb_wd.toString()); + + +adviewDFP.getQueryParams = function(e){ + var n = new RegExp("[\\?&]"+e+"=([^&#]*)").exec(window.location.href); + return null !== n ? n[1] : null +}; + + +if (adviewDFP.getQueryParams('dfp_target_kw')) adviewDFP.DFPTargeting.push('kw|'+adviewDFP.getQueryParams('dfp_target_kw')); + +var + kwString = '', + paramString = ''; + +for(var i = 0; i < adviewDFP.DFPTargeting.length; i++) { + var + _this = adviewDFP.DFPTargeting[i], + _thisParam = _this.split('|'); + + if (_this.indexOf('kw') !== -1) { + if (kwString.indexOf(_thisParam[1]) === -1) kwString += '%2C' + _thisParam[1]; + } else { + paramString += '%26'+_thisParam[0]+'%3D'+_thisParam[1]; + } +} + +//_YB +var _YB = window.top._YB || { ab:function(){return (Math.random()>=0.1?"b":"a"+Math.floor(Math.random()*10));} }; + +paramString += '%26yb_ab%3D' + _YB.ab(); +//_YB +paramString += '%26yb_ff%3D' + String(Math.round(Math.random())); + +if (typeof activeSubscription !== 'undefined' && activeSubscription) { + paramString += '%26subscription%3Dtrue'; +} + +if (dfpParams && dfpParams.video && dfpParams.video.preroll) { + dfpParams.video.preroll = dfpParams.video.preroll.replace('%26dystrybutor', kwString + paramString + '%26dystrybutor'); + dfpParams.video.preroll = dfpParams.video.preroll.replace('[adview_hostname]', escape(location.host)); + dfpParams.video.preroll = dfpParams.video.preroll.replace('[cb]', window.AG.rodoAccepted); + dfpParams.video.preroll = dfpParams.video.preroll.replace('[locationhref]', escape(window.location.href)); +} + +if(dfpParams && dfpParams.video && dfpParams.video.skin) dfpParams.video.skin = dfpParams.video.skin.replace('%26dystrybutor', kwString + paramString + '%26dystrybutor'); +if(dfpParams && dfpParams.video && dfpParams.video.overlay) dfpParams.video.overlay = dfpParams.video.overlay.replace('%26dystrybutor', kwString + paramString + '%26dystrybutor'); +if(dfpParams && dfpParams.video && dfpParams.video.pausead) dfpParams.video.pausead = dfpParams.video.pausead.replace('%26dystrybutor', kwString + paramString + '%26dystrybutor'); + + + + + + + + + + if (typeof randRodoAB !== 'undefined') { + googletag.pubads().setTargeting("kw", "testyab_rodo_" + randRodoAB); + adviewDFP.DFPTargeting.push('kw|testyab_rodo_' + randRodoAB); +} + +(function(){ + var _pm = window.location.search.match(/\?i=([0-9]+)/); + if (window.adviewDFP && adviewDFP.DFPTargeting && _pm) adviewDFP.DFPTargeting.push("yb_page|" + String(_pm[1])); + if (_pm) return googletag.pubads().setTargeting('yb_page', String(_pm[1])); +})(); + +googletag.pubads().setTargeting('domena', ""+window.top.location.hostname+""); + +var + adviewKW = []; + +if(typeof window.gExVariation !== 'undefined') { + adviewKW.push('testyab_' + gExVariation); + if (window.adviewDFP && adviewDFP.DFPTargeting) adviewDFP.DFPTargeting.push('kw|testyab_' + gExVariation); +} +if (/\/2,|\/14,/.test(window.location.href)) { + adviewKW.push('relacja'); + if (window.adviewDFP && adviewDFP.DFPTargeting) adviewDFP.DFPTargeting.push('kw|relacja'); +} +if (/\/15,/.test(window.location.href)) { + adviewKW.push('quiz-new'); + if (window.adviewDFP && adviewDFP.DFPTargeting) adviewDFP.DFPTargeting.push('kw|quiz-new'); +} +if (/\/13,/.test(window.location.href)) { + adviewKW.push('quiz-old'); + if (window.adviewDFP && adviewDFP.DFPTargeting) adviewDFP.DFPTargeting.push('kw|quiz-old'); +} +if (/\/5,/.test(window.location.href)) { + adviewKW.push('jsp-5'); + if (window.adviewDFP && adviewDFP.DFPTargeting) adviewDFP.DFPTargeting.push('kw|jsp-5'); +} +if (/\/51,/.test(window.location.href)) { + adviewKW.push('jsp-51'); + if (window.adviewDFP && adviewDFP.DFPTargeting) adviewDFP.DFPTargeting.push('kw|jsp-51'); +} +if (typeof adviewKW !== 'undefined' && adviewKW.length) { + googletag.pubads().setTargeting('kw', adviewKW); +} + + + + + + + + + + + + + + + + + + + + + + if(window.AG.rodoAccepted === -1) { + googletag.pubads().setRequestNonPersonalizedAds(1); + } + + googletag.pubads().enableSingleRequest(); + googletag.pubads().enableSyncRendering(); + googletag.pubads().collapseEmptyDivs(); + <!-- IBA Analytics --> + googletag.pubads().addEventListener('slotRenderEnded', function(data) { + adviewDFP.adviewRendered(data); + if(typeof IBA != 'undefined' && typeof IBA.dfp != 'undefined') IBA.dfp.onSlotRendered(data); + }); + if(window.AG.rodoAccepted !== 0) { + googletag.enableServices(); + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + }); + + function putBanDFP(a,b,c,d){if(!b||"string"!=typeof b)return!1;if(!dfpParams.slots[a])return console.warn("dfpParams slot: "+a+" not exists"),!1;var e=window.location.protocol+"//"+window.location.host+"/dfptools/adview/",f=e+dfpParams.slots[a].url;c&&"object"==typeof c&&(f=f.replace(/adUnitSize=\[\[(.*?)\]]/g,"adUnitSize="+JSON.stringify(c))),d&&"string"==typeof d&&(f+="&addKeysKw="+d),adviewDFP.iframeBan(b,f,e+"autoslot.js",0,0)}function putBanDFPInView(a,b,c,d){var e=parseInt(c)?parseInt(c):300;adviewDFP.DFPTargeting.push("kw|oldScroll"),window.addEventListener("scroll",function(){adviewDFP.isElementInViewport(b,e)&&(putBanDFP(a,b,d),this.removeEventListener("scroll",arguments.callee,!1))})}function putBanDFPInViewObject(a){if(!a.slot||!a.divId)return a.slot||console.warn("Slot ID is empty"),a.divId||console.warn("divId id empty"),!1;"021-IMK"!==a.slot&&"067-RECTANGLE-BTF"!==a.slot&&"035-RECTANGLE-BTF"!==a.slot||(a.margin=parseInt(adviewDFP.randomMargin),a.kw?a.kw+=", margin_"+adviewDFP.randomMargin:a.kw="margin_"+adviewDFP.randomMargin);var b=parseInt(a.margin)?parseInt(a.margin):300;window.addEventListener("scroll",function(){adviewDFP.isElementInViewport(a.divId,b)&&(putBanDFP(a.slot,a.divId,a.unitSize,a.kw),this.removeEventListener("scroll",arguments.callee,!1))})}function putBan(){}var adviewDFP=adviewDFP||{};adviewDFP.scrollSlot=[],adviewDFP.DFPTargeting=adviewDFP.DFPTargeting||[],adviewDFP.allSlotsForRODO=[],adviewDFP.arrayLabel=["001-TOPBOARD","003-RECTANGLE","000-MAINBOARD","042-FOOTBOARD","087-ADBOARD-A","071-WINIETA","004-PAYPER","007-CONTENTBOARD","021-IMK","059-BUTTON","078-STYLBOARD","044-BIZBOARD","035-RECTANGLE-BTF","067-RECTANGLE-BTF","101-TOPBOARD-MOBI","104-RECTANGLE-MOBI","107-MAINBOARD-MOBI","150-BIZBOARD-MOBI","151-FUNBOARD-MOBI","152-STYLBOARD-MOBI","153-MOTOBOARD-MOBI","150-ADBOARD-A-MOBI","150-ADBOARD-B-MOBI","150-ADBOARD-C-MOBI","150-ADBOARD-D-MOBI","108-FOOTBOARD-MOBI","000-SPORTBOARD","076-MAINBUTTON-0","111-BIZBUTTON","076-MAINBUTTON","116-SPORTBUTTON"],adviewDFP.check=!0,adviewDFP.init=function(){var a=setInterval(function(){if("complete"===document.readyState){clearInterval(a);var b=document.getElementsByClassName("activeBan"),c=document.getElementsByTagName("body")[0];-1!==window.navigator.userAgent.indexOf("Firefox")&&adviewDFP.firefox(),adviewDFP.check&&adviewDFP.checkAgain(),adviewDFP.checkBan(),adviewDFP.adBlock();for(var d=0;d<b.length;d++){var e=b[d],f=[parseInt(e.getAttribute("data-ad-width")),parseInt(e.getAttribute("data-ad-height"))];if(1===f[0]&&(f=[e.scrollWidth,e.scrollHeight]),"007-CONTENTBOARD"!==e.id||750!==f[0]||"pageTypeId_7"!==c.id&&"pageTypeId_1"!==c.id||-1!==window.location.href.indexOf("0,0.")?"007-CONTENTBOARD"!==e.id||750===f[0]||"pageTypeId_7"!==c.id&&"pageTypeId_1"!==c.id||adviewDFP.cleanWideContentBoard(e.id):adviewDFP.wideContentBoard(e.id,f),("pageTypeId_7"===c.id||"pageTypeId_1"===c.id)&&"007-CONTENTBOARD"===e.id){750!==document.getElementById("div-gpt-ad-007-CONTENTBOARD-0").clientWidth&&adviewDFP.cleanWideContentBoard(e.id)}}}},100)},adviewDFP.adviewRendered=function(a){var b=a.slot,c=null;Object.keys(b).map(function(a,d){var e=b[a];"string"==typeof e&&e.length&&-1!==e.indexOf("/75224259/")&&(c=e)}),"string"==typeof c&&c.length||console.warn("DFP slot is empty");var d=/[0-9]{3}-(.+)/,e=d.exec(c)[0],f=document.getElementById(e),g=document.getElementById("div-gpt-ad-"+e+"-0"),h=f.getElementsByTagName("span")[0],i=f.getElementsByTagName("iframe")[0],j=document.getElementsByTagName("body")[0];if(void 0!==window.top.dfpRendered&&window.top.dfpRendered({active:a.isEmpty,id:e,height:a.size?a.size[1]:"creative"}),a.isEmpty)return!1;-1===f.className.indexOf("activeBan")&&(f.className+=" activeBan"),f.setAttribute("data-ad-width",a.size[0]),f.setAttribute("data-ad-height",a.size[1]),a.size[0]>10&&(g.style.maxWidth=a.size[0]+"px",h.style.maxWidth=a.size[0]+"px"),h.style.marginLeft="auto",h.style.marginRight="auto",window.dfpParams.adsType&&window.dfpParams.adsType.scrollBanner&&adviewDFP.setClassToBody("scrollBannerDFP"),adviewDFP.setLabel(f),dfpParams.iframeResponsive&&i.getAttribute("responsive-iframe")&&adviewDFP.setIframeResponsive(i),"007-CONTENTBOARD"!==e||750!==a.size[0]||"pageTypeId_7"!==j.id&&"pageTypeId_1"!==j.id||-1!==window.location.href.indexOf("0,0.")?"007-CONTENTBOARD"!==ban.id||750===banSize[0]||"pageTypeId_7"!==j.id&&"pageTypeId_1"!==j.id||adviewDFP.cleanWideContentBoard(e):adviewDFP.wideContentBoard(e,a.size),"071-WINIETA"===e&&-1!==adviewDFP.arrayLabel.indexOf(e)&&adviewDFP.setWinieta()},adviewDFP.setClassToBody=function(a){var b=document.getElementsByTagName("body")[0];-1===b.className.indexOf(a)&&(b.className+=" "+a)},adviewDFP.wideContentBoard=function(a,b){var c=document.getElementById(a),d=document.getElementById("div-gpt-ad-"+a+"-0"),e=c.getElementsByClassName("banLabel")[0];-1===c.className.indexOf("wideContentBoard")&&(c.className+=" wideContentBoard"),c.style.marginBottom="30px",c.style.height=b[1]+e.clientHeight+"px",d.style.position="absolute",d.style.right=0},adviewDFP.cleanWideContentBoard=function(a){var b=document.getElementById(a),c=document.getElementById("div-gpt-ad-"+a+"-0");b.style.cssText="",c.style.cssText="",b.classList.remove("wideContentBoard")},adviewDFP.setWinieta=function(){var a=document.getElementById("pageHead"),b=document.createElement("span");b.className="banLabel winieta-lab",b.appendChild(document.createTextNode("REKLAMA")),a.appendChild(b)},adviewDFP.checkBan=function(){for(var a=document.getElementsByClassName("adviewDFPBanner"),b=0;b<a.length;b++){var c=a[b];c.offsetHeight>40&&adviewDFP.setLabel(c)}},adviewDFP.setLabel=function(a){var b=a,c=b.getElementsByClassName("banLabel")[0];if(c){if(-1!==adviewDFP.arrayLabel.indexOf(b.id)&&b.offsetHeight>40){c.style.display="block";for(var d=b.getElementsByTagName("*"),e=0,f=0;f<d.length;f++){var g=d[f],h=parseInt(g.style.width);h&&h>e?e=h:parseInt(g.width)&&parseInt(g.width)>e&&(e=g.width)}c.style.maxWidth=e+"px"}"undefined"!=typeof abp&&abp&&-1===b.className.indexOf("adblock")&&(b.className+=" adblock"),-1===b.className.indexOf("activeBan")&&b.offsetHeight>40&&(b.className+=" activeBan")}},adviewDFP.firefox=function(){var a=document.getElementsByClassName("banIndexDFP")[0];a&&(a.style.display="block",setTimeout(function(){"0px"===a.getElementsByTagName("iframe")[0].style.height&&a.parentNode.removeChild(a)},500))},adviewDFP.adBlock=function(){for(var a=["div-001-TOPBOARD-ABP","div-003-RECTANGLE-ABP","div-000-MAINBOARD-ABP","div-087-ADBOARD-A-ABP","div-087-ADBOARD-B-ABP","div-111-BIZBUTTON-ABP"],b=0;b<a.length;b++){var c=document.getElementById(a[b]);if(c&&c.clientHeight>40){var d=document.createElement("span");d.className="banLabel",d.appendChild(document.createTextNode("Reklama")),c.insertBefore(d,c.getElementsByTagName("div")[0]),-1===c.className.indexOf("activeBan")&&(c.className+="activeBan")}}},adviewDFP.iframeBan=function(a,b,c,d,e){if(!a)return!1;for(var f=document,g=f.getElementById(a);g.hasChildNodes();)g.removeChild(g.lastChild);var h=f.createElement("iframe"),i=f.createElement("div");i.className="fifContainer fif-container-"+a,h.src=b,h.style.width=d+"px",h.style.height=e+"px",h.style.margin=0,h.style.borderWidth=0,h.style.padding=0,h.scrolling="no",h.frameBorder=0,h.allowTransparency=!0,h.className="banDfpFIF",h.id=a+"_FIF",h.EAS_src=c,h.setAttribute("data-type","fif"),i.appendChild(h),g.appendChild(i)},adviewDFP.checkAgain=function(){setTimeout(function(){adviewDFP.checkBan()},3e3)},adviewDFP.onElementHeightChange=function(a,b){var c,d=a.clientHeight;!function e(){c=a.clientHeight,d!=c&&b(c),d=c,a.onElementHeightChangeTimer&&clearTimeout(a.onElementHeightChangeTimer),a.onElementHeightChangeTimer=setTimeout(e,200)}()},adviewDFP.scrollRect=function(){var a=document.getElementById("col_left");if(!a)return!1;var b=a.clientHeight,c=document.getElementById("col_right"),d=document.getElementById("holder_301"),e=document.getElementsByTagName("body")[0];c.style.minHeight=b+"px",d.style.minHeight=b+"px",document.getElementById("page").style.overflow="initial",-1===e.className.indexOf("scroll035Rectangle")&&(e.className+=" scroll035Rectangle")},adviewDFP.findClosestClass=function(a,b){for(;(a=a.parentElement)&&!a.classList.contains(b););return a},adviewDFP.setIframeResponsive=function(a){var b=a,c=b.getAttribute("data-prop"),d=parseInt(b.getAttribute("width")),e=parseInt(b.contentWindow.document.body.clientWidth);document.getElementsByTagName("body")[0].className,parseInt(adviewDFP.findClosestClass(b,"adviewDFPBanner").getAttribute("data-ad-height"));if(c||(c=parseInt(b.getAttribute("height"))/d,b.setAttribute("data-prop",c)),e!=d){var f=Math.round(e*c);b.style.height=f+"px"}},adviewDFP.getMobileIframe=function(){var a=document.querySelectorAll('iframe[id^="google_ads_iframe"]');if(!a.length&&!dfpParams.iframeResponsive)return!1;for(var b=0;b<a.length;b++){var c=a[b];c.clientHeight>40&&c.getAttribute("responsive-iframe")&&adviewDFP.setIframeResponsive(c)}},adviewDFP.removeDefaultLabel=function(a){var b=adviewDFP.arrayLabel.indexOf(a);-1!==adviewDFP.arrayLabel&&adviewDFP.arrayLabel.splice(b,1)},adviewDFP.isElementInViewport=function(a,b){var c=document.getElementById(a);if(!c)return console.warn('(DFP) ID: "'+a+'" not exists'),!1;var d=c.getBoundingClientRect();return b=b?Number(b):300,d.bottom>0&&d.right>0&&d.left<(window.innerWidth||document.documentElement.clientWidth)&&d.top<(window.innerHeight||document.documentElement.clientHeight)+b},window.onresize=function(){document.getElementsByTagName("body")[0];dfpParams.iframeResponsive&&adviewDFP.getMobileIframe()},adviewDFP.init(),document.addEventListener("DOMContentLoaded",function(){document.getElementsByTagName("body")[0].className+=" dfp-"+dfpParams.dir,adviewDFP.randomLoadMargin(),dfpParams.isMobile||-1!==window.location.href.indexOf("wyborcza.pl")||document.getElementById("content_wrap")&&(adviewDFP.scrollRect(),adviewDFP.onElementHeightChange(document.getElementById("content_wrap"),function(){-1===window.location.href.indexOf("bryla.pl")&&adviewDFP.scrollRect()})),adviewDFP.scrollSlot.forEach(function(a){putBanDFPInViewObject({slot:a,divId:a})},this)}),adviewDFP.randomLoadMargin=function(){var a=[0,100,200,300,400,500,600,700,800,900,1e3];adviewDFP.randomMargin=a[Math.floor(Math.random()*a.length)]},adviewDFP.setAdsForRodo=function(){};var head=document.head||document.getElementsByTagName("head")[0],style=document.createElement("style"),css=".innerContentWrapper button:nth-of-type(2){ display: {%VALUE%} !important; }";css=-1!==window.location.href.indexOf("ugotuj.to")||-1!==window.location.href.indexOf("sport.pl")?css.replace("{%VALUE%}","block"):css.replace("{%VALUE%}","none"),style.type="text/css",style.styleSheet?style.styleSheet.cssText=css:style.appendChild(document.createTextNode(css)),head.appendChild(style); + + + + + + + + + + + + + + + + // [HB] Core-prod-4 - HEADER_END +var _sts = new Date(); +var _sleeptime = PREBID_TIMEOUT - (_sts - _st0); +if(_sleeptime > 0){ + document.write('<scr'+'ipt type="text/javascript" src="https://g.gazeta.pl/przerwa?ttl=' + _sleeptime + '"></scr'+'ipt>'); +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + if(lazyGPT) { + + (function() { + var useSSL = 'https:' == document.location.protocol; + var src = (useSSL ? 'https:' : 'http:') + + '//www.googletagservices.com/tag/js/gpt.js'; + document.write('<scr' + 'ipt src="' + src + '"></scr' + 'ipt>'); + })(); + } + + </script> + + + + + + +<style type="text/css">.adviewDFPBanner{text-align:center}.banLabel{text-transform:uppercase;margin-top:6px;font:normal 10px Arial,sans-serif;padding-bottom:2px;text-align:left;color:#999}.adviewDFPBanner.activeBan,.adviewDFPBanner.activeBan .banLabel{display:block}.adviewDFPBanner.activeBan{padding:5px 0}.adviewDFPBanner.activeBan>div{margin-left:auto;margin-right:auto}body.screeningLabel-001-TOPBOARD .DFP-001-TOPBOARD{padding-top:5px}body.screeningLabel-001-TOPBOARD .DFP-001-TOPBOARD .banLabel{position:absolute;top:35px;left:10px;max-width:none!important}@media screen and (max-width:1320px){body#pageTypeId_7.screeningLabel-001-TOPBOARD .DFP-001-TOPBOARD{position:relative}body#pageTypeId_7.screeningLabel-001-TOPBOARD .DFP-001-TOPBOARD .banLabel{top:0;left:-53px}}.adviewDFPBanner.activeBan a:hover{background:0 0!important}.DFP-007-CONTENTBOARD{width:100%;position:relative}body.winieta #page-top:not(.fixed){position:relative}body.winieta #page-top:not(.fixed) #pageHead .imgw img{position:absolute;top:0;left:0;z-index:2}body.winieta #page-top:not(.fixed) #pageHead.hasBanner{min-height:90px}#pageHead.hasBanner .banLabel{position:absolute;top:20px;right:-40px;padding:3px 5px;margin:0;transform:rotate(-90deg);background:#fff}body.dfp-forum #pageHead.hasBanner .banLabel{right:-36px}body.dfp-forum.winieta{width:auto!important}#pageHead.hasBanner .column.col1{display:inline-block;position:relative;z-index:2;height:100%}body.dfp-gazetawyborcza #pageHead.hasBanner .c0{top:40px;position:absolute}.DFP-091-RELATED .kd_ns_logo{background:0 0!important;width:auto!important;height:auto!important;margin:auto!important}#div-gpt-ad-091-RELATED-0{max-width:100%!important}body.screeningADFP #page{position:initial}.isScreening #page-top{cursor:pointer}body.screeningADFP.desk .DFP-001-TOPBOARD.activeBan{background:0 0}body.scrollBannerDFP .DFP-001-TOPBOARD .banLabel{position:absolute}#article-list .banner,.indexBanner,div.banIndexDFP,li.banIndexDFP,li.entry.banIndexDFP{display:none}div[id*="-MOBI"].adviewDFPBanner a img{width:100%;height:auto}div[id*="-MOBI"].adviewDFPBanner iframe[responsive-iframe=true]{max-width:100%;width:100%!important;height:auto}body.dfp-mobiHP .banLabel,body.dfp-mobiHP div[id*=div-gpt-ad-]{max-width:100%!important}.dfp-video-bg #page{overflow:initial}.dfp-video-bg .photostoryNextPage,.dfp-video-bg .photostoryPrevPage{z-index:1!important}#div-001-TOPBOARD-ABP .banLabel,#div-003-RECTANGLE-ABP .banLabel{display:block!important}body.scroll035Rectangle .DFP-035-RECTANGLE-BTF{position:sticky!important;position:-webkit-sticky!important;position:-moz-sticky!important;top:40px!important}body.socialContent .DFP-001-TOPBOARD,body.socialContent .DFP-101-TOPBOARD-MOBI{position:relative}body.socialContent .DFP-001-TOPBOARD.activeBan .banLabel,body.socialContent .DFP-101-TOPBOARD-MOBI.activeBan .banLabel{width:100%;position:absolute;left:0;top:0;margin:0;padding:10px;box-sizing:border-box;font-family:Roboto,sans-serif;font-size:8px;letter-spacing:.7px;max-width:none!important;background:-webkit-linear-gradient(rgba(0,0,0,.2),rgba(0,0,0,0));background:-o-linear-gradient(rgba(0,0,0,.2),rgba(0,0,0,0));background:-moz-linear-gradient(rgba(0,0,0,.2),rgba(0,0,0,0));background:linear-gradient(rgba(0,0,0,.2),rgba(0,0,0,0))}body.socialContent .DFP-007-CONTENTBOARD .banLabel,body.socialContent .DFP-104-RECTANGLE-MOBI .banLabel{display:none!important}body.socialContent .DFP-101-TOPBOARD-MOBI>div,body.socialContent .DFP-104-RECTANGLE-MOBI>div{max-width:none!important}body[id^=pageTypeId_0] .adviewDFPBanner.activeBan,body[id^=pageTypeId_56] .adviewDFPBanner.activeBan,body[id^=pageTypeId_7] .adviewDFPBanner.activeBan{margin-bottom:30px;padding-bottom:0}.desk .adviewDFPBanner.DFP-001-TOPBOARD.activeBan{margin-bottom:15px}body.alternative .adviewDFPBanner.activeBan{padding-top:0!important;padding-bottom:20px!important;margin-top:0!important;margin-bottom:0!important}body.alternative .adviewDFPBanner.activeBan .banLabel{padding-top:15px!important;padding-bottom:10px!important;margin-top:0!important;margin-bottom:0!important}body.alternative .DFP-003-RECTANGLE.adviewDFPBanner.activeBan .banLabel{padding-top:0!important}body.alternative .article .adviewDFPBanner.activeBan{margin-bottom:20px!important}body.alternative .DFP-035-RECTANGLE-BTF,body.alternative .DFP-067-RECTANGLE-BTF{position:sticky;top:30px}#banC1.activeBan,#banC2.activeBan,#banC3.activeBan,#banC4.activeBan,#banC9.activeBan{position:relative}#banC1.wideContentBoard>iframe,#banC2.wideContentBoard>iframe,#banC3.wideContentBoard>iframe,#banC4.wideContentBoard>iframe,#banC9.wideContentBoard>iframe{position:absolute;right:0}#article_body #banC1.wideContentBoard iframe,#article_body #banC2.wideContentBoard iframe,#article_body #banC3.wideContentBoard iframe,#article_body #banC4.wideContentBoard iframe,#article_body #banC9.wideContentBoard iframe{max-width:initial}.DFP-201-PREMIUMBOARD-MOBI.activeBan,.DFP-201-PREMIUMBOARD.activeBan{display:none}@media screen and (orientation:portrait){div.oaoa__interstitial{height:100%!important}}@media all and (max-width:330px){.responsive .adviewDFPBanner.activeBan{padding-left:0!important;padding-right:0!important}}.DFP-019-TOPLAYER{margin:0!important}body.path_sport-hp .DFP-087-ADBOARD-A{position:relative;z-index:9}body.path_sport-hp .DFP-087-ADBOARD-A .banLabel{background:linear-gradient(rgba(255,255,255,0),#fff)}body.path_sport-hp .DFP-087-ADBOARD-A:before{content:"";position:absolute;bottom:-7px;left:0;width:100%;height:13px;background:linear-gradient(#fff,rgba(255,255,255,0))}.desk.screeningADFP.none #content #content_wrap{margin-top:0!important}body.screeningADFP.dfp-czterykaty #bottom_wrapper,body.screeningADFP.dfp-czterykaty #main_wrapper,body.screeningADFP.dfp-czterykaty #top_wrapper,body.screeningADFP.dfp-czterykaty .aside_wrapper{max-width:1242px;margin:0 auto}.fifContainer{font-size:.1px;line-height:0}.DFP-113-LOKALBUTTON{overflow:visible!important}</style> +<!-- v2.6 --> +
+
+<!-- v2.8 adapter biznes /biznes-->
+ +<!--10185226, [ /tpl/ads/prod/dfpHeader.jsp ], dfpBanersHeaderBean--> +
+
+
+
+
+<!-- facebookConnect v1.41 -->
+
+
+
+
+
+
+<!--[if IE]><script>(function(t) { for (var i = 0, l = t.length; i < l; i++) document.createElement(t[i]); })(['fb','og']);</script><![endif]-->
+
+
+
+ <div id="fb-root"></div>
+ <script>
+ window.gazeta_pl = window.gazeta_pl || {};
+ window.gazeta_pl.functionQueue = window.gazeta_pl.functionQueue || [];
+ window.gazeta_pl.functionQueue.push(function(){
+ setTimeout(function() {
+ (function(d){
+ var js, id = 'facebook-jssdk'; if (d.getElementById(id)) {return;}
+ js = d.createElement('script'); js.id = id; js.async = true;
+ js.src = "//connect.facebook.net/pl_PL/all.js";
+ d.getElementsByTagName('head')[0].appendChild(js);
+ }(document));
+ }, 2000);
+ function facebookReady(){
+ FB.init({
+ appId : 515714931781741,
+ status : true,
+ cookie : true,
+ xfbml : true,
+ oauth : true,
+ version: 'v2.2'
+ });
+ }
+ if(window.FB) {
+ facebookReady();
+ } else {
+ window.fbAsyncInit = facebookReady;
+ }
+ });
+ </script>
+
+
+ +<!--9638937, [ /fix/modules/facebook/facebookConnect.jsp ], null--> +
+</head>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<body id="pagetype_art" class=" awd path_147743 path_biznes simpleArt unknown Other unknown LINUX ">
+
+ <!-- pattern#ms - Article 7 - app18:tomcat-wyborcza: - - 3200118 - s3200007_P20_wyb_2017_desk.jsp - v1.0.1 -->
+
+
+
+<!-- WyborczaSVG version: 17.0.28 compiled: 2018-08-28 10:52:39 --> <svg id="svgicons" style="display:block;width:0;height:0;"><symbol id="admin-censore" viewBox="0 0 19.5 19.5"><path d="M438,444.25a9.75,9.75,0,1,1,9.75-9.75A9.76,9.76,0,0,1,438,444.25Zm0-17a7.25,7.25,0,1,0,7.25,7.25A7.26,7.26,0,0,0,438,427.25Z" transform="translate(-428.25 -424.75)"/><path d="M430.52,436.84L443.74,429l1.59,2.71-13.22,7.81Z" transform="translate(-428.25 -424.75)"/></symbol><symbol id="admin-clear" viewBox="0 0 19.63 16.99"><path d="M446.86,418.74v-2h-2.61l-6.12-6.22-4.76,4.71h0l-1.41,1.41h0l-4.71,4.66,6.12,6.22,4.76-4.71h0l1.41-1.41h0l2.68-2.65h4.64Zm-13.48,5.51-2.9-2.94,3.09-3.06,2.92,2.92Zm4.53-4.48L435,416.85l3.11-3.08,2.9,2.94Z" transform="translate(-427.24 -410.51)"/></symbol><symbol id="admin-delete" viewBox="0 0 166.86 170"><polygon points="73.5 170 0 170 0 31.87 30.6 0 141 0 141 86.28 118 86.28 118 23 40.4 23 23 41.13 23 147 73.5 147 73.5 170"/><rect x="33.5" y="40.5" width="75" height="13"/><rect x="33.5" y="65.5" width="75" height="13"/><polygon points="166.86 158.06 140.19 132.39 166.86 104.71 155.67 93.53 129 120.2 102.33 93.53 91.14 104.71 117.81 131.39 91.14 158.06 102.33 169.25 129 142.57 155.67 169.25 166.86 158.06"/></symbol><symbol id="admin-expose" viewBox="0 0 36.54 34.75"><polygon points="18.27 0 23.92 11.44 36.54 13.27 27.41 22.18 29.56 34.75 18.27 28.81 6.98 34.75 9.13 22.18 0 13.27 12.62 11.44 18.27 0"/></symbol><symbol id="admin-publish" viewBox="0 0 19.5 19.5"><polygon points="14.44 8.91 10.59 8.91 10.59 5.06 8.91 5.06 8.91 8.91 5.06 8.91 5.06 10.59 8.91 10.59 8.91 14.44 10.59 14.44 10.59 10.59 14.44 10.59 14.44 8.91"/><path d="M423,489.33a9.75,9.75,0,1,1,9.75-9.75A9.75,9.75,0,0,1,423,489.33Zm0-17a7.25,7.25,0,1,0,7.25,7.25h0A7.26,7.26,0,0,0,423,472.33Z" transform="translate(-413.25 -469.83)"/></symbol><symbol id="admin-undo-expose" viewBox="0 0 16 13.99"><polygon points="16 2.54 6.47 2.54 7.38 1.5 6.08 0 0 7 1.3 8.49 1.3 8.49 6.08 13.99 7.38 12.49 6.47 11.45 16 11.45 16 2.54"/></symbol><symbol id="app-store" viewBox="0 0 135 40"><g><path class="store-frame" d="m130.2 40h-125.47c-2.608 0-4.73-2.128-4.73-4.733v-30.541c0-2.606 2.122-4.726 4.729-4.726h125.47c2.6 0 4.8 2.12 4.8 4.726v30.541c0 2.605-2.2 4.733-4.8 4.733z" fill="#A6A6A6"/><path d="m134.03 35.268c0 2.116-1.714 3.83-3.834 3.83h-125.47c-2.119 0-3.839-1.714-3.839-3.83v-30.543c0-2.115 1.72-3.835 3.839-3.835h125.47c2.121 0 3.834 1.72 3.834 3.835v30.543z" class="store-bkg"/></g><g fill="#fff"><path d="m30.128 19.795c-0.029-3.223 2.639-4.791 2.761-4.864-1.511-2.203-3.853-2.504-4.676-2.528-1.967-0.207-3.875 1.177-4.877 1.177-1.022 0-2.565-1.157-4.228-1.123-2.14 0.033-4.142 1.272-5.24 3.196-2.266 3.923-0.576 9.688 1.595 12.858 1.086 1.553 2.355 3.287 4.016 3.226 1.625-0.067 2.232-1.036 4.193-1.036 1.943 0 2.513 1.036 4.207 0.997 1.744-0.028 2.842-1.56 3.89-3.127 1.255-1.78 1.759-3.533 1.779-3.623-0.04-0.013-3.386-1.29-3.42-5.153z"/><path d="m26.928 10.317c0.874-1.093 1.472-2.58 1.306-4.089-1.265 0.056-2.847 0.875-3.758 1.944-0.806 0.942-1.526 2.486-1.34 3.938 1.421 0.106 2.88-0.716 3.792-1.793z"/></g><g fill="#fff"><path d="m53.646 31.516h-2.271l-1.244-3.91h-4.324l-1.185 3.91h-2.211l4.284-13.308h2.646l4.305 13.308zm-3.89-5.549l-1.125-3.475c-0.119-0.355-0.342-1.191-0.671-2.508h-0.04c-0.131 0.566-0.342 1.402-0.632 2.508l-1.105 3.475h3.573z"/><path d="m64.663 26.599c0 1.632-0.441 2.923-1.323 3.87-0.79 0.842-1.771 1.264-2.942 1.264-1.264 0-2.172-0.455-2.725-1.363h-0.04v5.055h-2.132v-10.347c0-1.026-0.027-2.079-0.079-3.158h1.875l0.119 1.52h0.04c0.711-1.145 1.79-1.717 3.238-1.717 1.132 0 2.077 0.447 2.833 1.342 0.757 0.895 1.136 2.073 1.136 3.534zm-2.172 0.079c0-0.935-0.21-1.705-0.632-2.311-0.461-0.631-1.08-0.947-1.856-0.947-0.526 0-1.004 0.175-1.431 0.523-0.428 0.349-0.708 0.807-0.839 1.372-0.066 0.264-0.099 0.48-0.099 0.651v1.6c0 0.697 0.214 1.287 0.642 1.768s0.984 0.721 1.668 0.721c0.803 0 1.428-0.311 1.875-0.928 0.448-0.619 0.672-1.436 0.672-2.449z"/><path d="m75.7 26.599c0 1.632-0.441 2.923-1.324 3.87-0.789 0.842-1.77 1.264-2.941 1.264-1.264 0-2.172-0.455-2.725-1.363h-0.039v5.055h-2.132v-10.347c0-1.026-0.027-2.079-0.079-3.158h1.875l0.119 1.52h0.04c0.711-1.145 1.789-1.717 3.238-1.717 1.131 0 2.076 0.447 2.834 1.342 0.755 0.895 1.134 2.073 1.134 3.534zm-2.172 0.079c0-0.935-0.211-1.705-0.633-2.311-0.461-0.631-1.078-0.947-1.855-0.947-0.527 0-1.004 0.175-1.432 0.523s-0.707 0.807-0.839 1.372c-0.065 0.264-0.099 0.48-0.099 0.651v1.6c0 0.697 0.214 1.287 0.641 1.768 0.428 0.48 0.984 0.721 1.67 0.721 0.803 0 1.428-0.311 1.875-0.928 0.448-0.619 0.672-1.436 0.672-2.449z"/><path d="m88.04 27.783c0 1.133-0.394 2.053-1.182 2.764-0.867 0.777-2.075 1.166-3.625 1.166-1.432 0-2.581-0.277-3.449-0.83l0.494-1.777c0.935 0.566 1.962 0.85 3.081 0.85 0.804 0 1.429-0.182 1.877-0.543 0.447-0.363 0.671-0.848 0.671-1.455 0-0.539-0.185-0.994-0.553-1.363s-0.98-0.713-1.836-1.029c-2.33-0.869-3.495-2.143-3.495-3.816 0-1.094 0.408-1.991 1.225-2.69 0.815-0.699 1.901-1.048 3.258-1.048 1.211 0 2.218 0.211 3.021 0.632l-0.533 1.738c-0.75-0.408-1.599-0.612-2.547-0.612-0.75 0-1.336 0.185-1.757 0.553-0.355 0.329-0.533 0.73-0.533 1.204 0 0.526 0.204 0.961 0.612 1.304 0.355 0.315 1 0.658 1.935 1.026 1.146 0.461 1.987 1.001 2.527 1.618s0.809 1.387 0.809 2.308z"/><path d="m95.088 23.519h-2.35v4.659c0 1.186 0.415 1.777 1.244 1.777 0.382 0 0.697-0.033 0.948-0.098l0.059 1.619c-0.421 0.156-0.974 0.236-1.658 0.236-0.843 0-1.501-0.258-1.975-0.77-0.474-0.514-0.711-1.377-0.711-2.588v-4.837h-1.401v-1.597h1.401v-1.758l2.093-0.632v2.39h2.35v1.599z"/><path d="m105.69 26.639c0 1.475-0.422 2.686-1.264 3.633-0.882 0.975-2.054 1.461-3.515 1.461-1.409 0-2.53-0.467-3.366-1.402-0.836-0.934-1.254-2.113-1.254-3.533 0-1.488 0.431-2.705 1.293-3.653s2.024-1.421 3.485-1.421c1.408 0 2.54 0.467 3.396 1.401 0.8 0.907 1.21 2.078 1.21 3.514zm-2.21 0.068c0-0.885-0.19-1.643-0.572-2.277-0.447-0.766-1.086-1.148-1.915-1.148-0.856 0-1.508 0.383-1.955 1.148-0.382 0.635-0.572 1.406-0.572 2.317 0 0.885 0.19 1.644 0.572 2.276 0.461 0.766 1.105 1.148 1.936 1.148 0.815 0 1.454-0.389 1.915-1.168 0.38-0.646 0.58-1.411 0.58-2.296z"/><path d="m112.62 23.795c-0.211-0.039-0.435-0.059-0.671-0.059-0.751 0-1.33 0.283-1.738 0.849-0.355 0.501-0.533 1.132-0.533 1.896v5.035h-2.132l0.02-6.575c0-1.106-0.026-2.112-0.079-3.021h1.856l0.079 1.836h0.059c0.224-0.632 0.579-1.139 1.066-1.521 0.474-0.343 0.987-0.513 1.54-0.513 0.197 0 0.375 0.013 0.533 0.039v2.034z"/><path d="m122.16 26.264c0 0.381-0.026 0.703-0.079 0.967h-6.396c0.025 0.947 0.335 1.672 0.928 2.172 0.539 0.447 1.237 0.672 2.093 0.672 0.947 0 1.81-0.152 2.587-0.455l0.335 1.48c-0.908 0.396-1.981 0.594-3.218 0.594-1.488 0-2.656-0.438-3.505-1.314-0.849-0.875-1.273-2.049-1.273-3.523 0-1.447 0.395-2.652 1.185-3.613 0.829-1.027 1.948-1.54 3.356-1.54 1.382 0 2.429 0.513 3.14 1.54 0.55 0.815 0.84 1.821 0.84 3.02zm-2.04-0.553c0.013-0.633-0.126-1.179-0.415-1.64-0.368-0.593-0.935-0.888-1.698-0.888-0.697 0-1.264 0.289-1.697 0.868-0.355 0.461-0.566 1.014-0.632 1.659h4.45z"/></g><g fill="#fff"><path d="m47.867 8.808c0 0.602-0.178 1.083-0.533 1.445-0.459 0.472-1.129 0.708-2.008 0.708-0.259 0-0.46-0.016-0.602-0.048v2.532h-1.048v-6.451c0.499-0.09 1.069-0.136 1.711-0.136 0.83 0 1.455 0.178 1.877 0.534 0.402 0.349 0.603 0.821 0.603 1.416zm-1.048 0.048c0-0.382-0.122-0.674-0.366-0.878-0.245-0.204-0.586-0.306-1.023-0.306-0.29 0-0.525 0.02-0.705 0.058v2.348c0.147 0.039 0.351 0.058 0.608 0.058 0.463 0 0.827-0.113 1.091-0.339s0.395-0.54 0.395-0.941z"/><path d="m53.727 11.048c0 0.725-0.207 1.319-0.621 1.785-0.434 0.479-1.009 0.718-1.727 0.718-0.692 0-1.243-0.229-1.654-0.689-0.41-0.459-0.615-1.038-0.615-1.736 0-0.73 0.211-1.329 0.635-1.794s0.994-0.698 1.712-0.698c0.692 0 1.248 0.229 1.669 0.688 0.399 0.446 0.601 1.022 0.601 1.726zm-1.087 0.035c0-0.435-0.094-0.808-0.281-1.119-0.22-0.376-0.533-0.564-0.94-0.564-0.421 0-0.741 0.188-0.961 0.564-0.188 0.311-0.281 0.69-0.281 1.138 0 0.435 0.094 0.808 0.281 1.119 0.227 0.376 0.543 0.564 0.951 0.564 0.4 0 0.714-0.191 0.94-0.574 0.194-0.318 0.291-0.694 0.291-1.128z"/><path d="m59.769 11.02c0 0.795-0.22 1.429-0.659 1.901-0.388 0.42-0.863 0.631-1.426 0.631-0.673 0-1.168-0.278-1.484-0.834h-0.02l-0.058 0.728h-0.893c0.025-0.381 0.038-0.805 0.038-1.271v-5.608h1.048v2.852h0.02c0.311-0.524 0.812-0.786 1.504-0.786 0.568 0 1.032 0.218 1.392 0.655 0.358 0.437 0.538 1.014 0.538 1.732zm-1.067 0.038c0-0.459-0.104-0.834-0.311-1.125-0.227-0.317-0.534-0.476-0.922-0.476-0.259 0-0.491 0.084-0.698 0.252s-0.346 0.391-0.417 0.669c-0.026 0.11-0.039 0.22-0.039 0.33v0.824c0 0.324 0.108 0.602 0.325 0.834s0.486 0.349 0.81 0.349c0.395 0 0.702-0.148 0.922-0.446 0.22-0.296 0.33-0.7 0.33-1.211z"/><path d="m62.485 7.324c0 0.188-0.062 0.339-0.184 0.456-0.123 0.117-0.281 0.175-0.476 0.175-0.175 0-0.322-0.06-0.441-0.179-0.12-0.12-0.18-0.27-0.18-0.451s0.062-0.33 0.185-0.446 0.274-0.175 0.456-0.175c0.181 0 0.333 0.059 0.456 0.175 0.123 0.115 0.184 0.264 0.184 0.445zm-0.116 6.12h-1.048v-4.714h1.048v4.714z"/><path d="m68.111 10.864c0 0.188-0.014 0.346-0.039 0.475h-3.142c0.013 0.466 0.164 0.821 0.455 1.067 0.266 0.22 0.608 0.33 1.028 0.33 0.466 0 0.89-0.074 1.271-0.223l0.164 0.728c-0.446 0.194-0.973 0.291-1.581 0.291-0.73 0-1.305-0.215-1.722-0.645s-0.625-1.007-0.625-1.731c0-0.711 0.193-1.303 0.582-1.775 0.407-0.504 0.956-0.756 1.648-0.756 0.679 0 1.193 0.252 1.542 0.756 0.28 0.4 0.419 0.895 0.419 1.483zm-1-0.271c0.007-0.311-0.062-0.579-0.203-0.805-0.182-0.291-0.459-0.437-0.834-0.437-0.343 0-0.621 0.142-0.835 0.427-0.174 0.227-0.277 0.498-0.31 0.815h2.182z"/><path d="m72.126 9.652c-0.104-0.02-0.213-0.029-0.33-0.029-0.368 0-0.652 0.139-0.854 0.417-0.174 0.246-0.262 0.556-0.262 0.931v2.474h-1.048l0.01-3.23c0-0.543-0.013-1.038-0.038-1.484h0.911l0.039 0.902h0.029c0.109-0.311 0.284-0.56 0.523-0.747 0.233-0.168 0.485-0.252 0.757-0.252 0.097 0 0.185 0.006 0.262 0.019v0.999z"/><path d="m76.83 13.444h-3.841v-0.611l1.882-2.473c0.116-0.155 0.329-0.411 0.64-0.767v-0.019h-2.338v-0.844h3.608v0.65l-1.843 2.435c-0.207 0.265-0.42 0.521-0.64 0.766v0.02h2.531v0.843z"/><path d="m87.365 8.73l-1.475 4.714h-0.96l-0.611-2.047c-0.155-0.511-0.281-1.019-0.379-1.523h-0.019c-0.091 0.518-0.217 1.025-0.379 1.523l-0.649 2.047h-0.971l-1.386-4.714h1.077l0.533 2.241c0.129 0.53 0.235 1.035 0.32 1.513h0.019c0.078-0.394 0.207-0.896 0.389-1.503l0.669-2.25h0.854l0.641 2.202c0.155 0.537 0.281 1.054 0.378 1.552h0.029c0.071-0.485 0.178-1.002 0.32-1.552l0.572-2.202h1.028z"/></g></symbol><symbol id="arrow-back" viewBox="0 0 17 9"><path d="M0,4.5L5.7,0v2.2C15.9,2.2,17,7.9,17,9C14.7,4.5,6.8,6.8,5.7,6.8V9L0,4.5z"/></symbol><symbol id="bell" viewBox="0 0 32 32"><path d="M15.7,29.5c-2.8,0-4-1.9-4.2-2.9c-0.1-0.2,0.1-0.5,0.3-0.5c0.2-0.1,0.5,0.1,0.5,0.3
+ c0.2,0.8,1.1,2.2,3.4,2.2c2.3,0,3.2-1.4,3.4-2.2c0.1-0.2,0.3-0.4,0.5-0.3c0.2,0.1,0.4,0.3,0.3,0.5C19.7,27.6,18.5,29.5,15.7,29.5z"/><path d="M26.2,22.8c-1.2-1-3.7-3.1-3.7-7.7c0-4.2-1.6-6.5-2.9-7.6c-1.2-1.1-2.6-1.5-3.4-1.6V3.8
+ c0-0.2-0.2-0.4-0.4-0.4c-0.2,0-0.4,0.2-0.4,0.4v2.1c-0.8,0.1-2.2,0.5-3.4,1.6C10.5,8.7,9,10.9,9,15.1c0,4.5-2.5,6.6-3.7,7.7
+ c-0.5,0.5-0.9,0.7-0.7,1.1c0.2,0.5,0.7,0.5,2.4,0.5h17.7c1.7,0,2.2,0,2.4-0.5C27,23.5,26.7,23.3,26.2,22.8z M24.5,23.6H6.8
+ c-0.3,0-0.8,0-1.2,0c0,0,0.1-0.1,0.1-0.1c1.2-1,4.1-3.4,4.1-8.3c0-3.1,0.9-5.5,2.6-7c1.4-1.2,2.8-1.4,3.3-1.4
+ c0.5,0,1.9,0.3,3.3,1.4c1.7,1.5,2.6,3.8,2.6,7c0,4.9,2.8,7.3,4.1,8.3c0,0,0.1,0.1,0.1,0.1C25.3,23.6,24.8,23.6,24.5,23.6z"/></symbol><symbol id="blog" viewBox="0 0 38.2 39.4"><path d="m 30.9,37.8 -23.5,0 -4.3,1.6 27.8,0 c 0.4,0 0.8,-0.3 0.8,-0.8 0,-0.5 -0.3,-0.8 -0.8,-0.8 z"/><path d="M 38.2,5.6 32.8,0 28.9,3.9 25.3,0.3 18.1,7.6 19.2,8.7 25.4,2.4 27.8,5 3.1,29.7 c 0,0 0,0 0,0 0,0 0,0 0,0 L 2.9,29.9 2.8,30 0,38.3 8.2,35.6 38.2,5.6 Z M 32.8,2.1 36.2,5.5 33.4,8.3 29.9,5 32.8,2.1 Z M 2.4,35.9 3.8,31.6 6.7,34.5 2.4,35.9 Z M 4.6,30.3 28.9,6 32.3,9.4 8,33.6 4.6,30.3 Z"/></symbol><symbol id="burger" viewBox="-338 41.4 32 32"><rect x="-330.4" y="61.7" width="16.8" height="1.5"/><rect x="-330.4" y="56.6" width="16.8" height="1.5"/><rect x="-330.4" y="51.6" width="16.8" height="1.5"/></symbol><symbol viewBox="0 0 19.5 30" id="chevron"><path d="M 15,0 0,15 15,30 19.5,25.5 9,15 19.5,4.5 Z M 15,1 18.5,4.5 8,15 18.5,25.5 15,29 1,15 Z"/></symbol><symbol id="clock" viewBox="0 0 15 15"><path d="M7.5,0A7.5,7.5,0,1,0,15,7.5,7.5,7.5,0,0,0,7.5,0Zm4.91,8H7V7.5H7V2.59H8V7h4.41Z"/></symbol><symbol id="close-symbol" viewBox="0 0 18 18"><path d="M 0.7,18 0,17.2 17.3,0 18,0.8 0.7,18 z M 0,0.7 0.8,0 18,17.3 17.2,18 0,0.7 z"/></symbol><symbol id="comment" viewBox="0 0 20 20"><path d="M16.7,13.6c1.4,0,2.5-1.1,2.5-2.5V3.5c0-1.3-1.2-2.5-2.5-2.5H3.8C2.3,0.9,1.3,2,1.3,3.5V11 c0,1.3,1.2,2.5,2.5,2.5h1.7V19l4.8-5.4C10.2,13.6,16.7,13.6,16.7,13.6z"/></symbol><symbol id="commentary" viewBox="0 0 17.9 18"><path d="m 15.4,12.606309 c 1.4,0 2.5,-1.1 2.5,-2.5 V 2.5063091 c 0,-1.3 -1.2,-2.49999999 -2.5,-2.49999999 H 2.5 C 1,-0.09369089 0,1.0063091 0,2.5063091 v 7.4999999 c 0,1.3 1.2,2.5 2.5,2.5 h 1.7 v 5.5 l 4.8,-5.4 c -0.1,0 6.4,0 6.4,0 z"/></symbol><symbol id="comments-v2" viewBox="0 0 34 34"><path d="M11,29v-6H5V8h24v15H17L11,29z"/><path d="M13.3,14.7c0.4,0,0.7,0.3,0.7,0.7s-0.3,0.7-0.7,0.7s-0.7-0.3-0.7-0.7S12.9,14.7,13.3,14.7z"/><path d="M17,14.7c0.4,0,0.7,0.3,0.7,0.7s-0.3,0.7-0.7,0.7s-0.7-0.3-0.7-0.7S16.6,14.7,17,14.7z"/><path d="M20.7,14.7c0.4,0,0.7,0.3,0.7,0.7s-0.3,0.7-0.7,0.7S20,15.8,20,15.4S20.3,14.7,20.7,14.7z"/></symbol><symbol id="diamond" viewBox="0 0 32 32"><path d="M16,29.2L0.9,14v-1.2l5.7-6.5h18.9l5.7,6.5V14L16,29.2z M1.9,13.6L16,27.8l14.1-14.1v-0.4L25,7.4H7l-5.1,5.8V13.6z"/><rect x="1.4" y="12.6" width="29.3" height="1"/><polygon points="16.5,28.7 15.5,28.3 22.4,13.1 19.4,7.1 20.3,6.7 23.5,13 "/><polygon points="15.5,28.7 8.5,13 11.7,6.7 12.6,7.1 9.6,13.1 16.5,28.3 "/></symbol><symbol id="dots" viewBox="0 0 27 7"><path d="M 3.5,0 C 1.6,0 0,1.6 0,3.5 0,5.4 1.6,7 3.5,7 5.4,7 7,5.4 7,3.5 7,1.6 5.4,0 3.5,0 z m 10,0 C 11.6,0 10,1.6 10,3.5 10,5.4 11.6,7 13.5,7 15.4,7 17,5.4 17,3.5 17,1.6 15.4,0 13.5,0 z m 10,0 C 21.6,0 20,1.6 20,3.5 20,5.4 21.6,7 23.5,7 25.4,7 27,5.4 27,3.5 27,1.6 25.4,0 23.5,0 z"/></symbol><symbol id="down-arrow" viewBox="0 0 21.2 14.1"><rect x="6.7" y="4.6" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -0.8498 12.1094)" width="15" height="5"/><rect x="4.6" y="-0.4" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -2.917 7.1)" width="5" height="15"/></symbol><symbol id="envelope" viewBox="0 0 50 32"><path d="M 48.1,-2.4512111e-7 H 2.1 1.7 0.7 0 V 32 H 50 V -2.4512111e-7 H 48.3 48.1 z M 46.1,1.9999998 26.7,21.3 c -0.9,0.9 -2.4,0.9 -3.3,0 L 4.1,1.9999998 h 42 z m -44,0.8 L 15.1,15.8 2.1,28.8 V 2.7999998 z M 3.7,30 16.5,17.2 22,22.7 c 0.8,0.8 1.9,1.3 3.1,1.3 1.2,0 2.2,-0.5 3.1,-1.3 L 33.7,17.2 46.5,30 H 3.7 z m 44.4,-1.2 -13,-13 13,-13.0000002 V 28.8 z"/></symbol><symbol id="eye" viewBox="0 0 32 20"><path d="M 16,0 C 7.2,0 0,8 0,10 0,11.8 7.2,20 16,20 24.8,20 32,12 32,10 32,8 24.8,0 16,0 z m 0,16 c -3.3,0 -6,-2.7 -6,-6 0,-3.3 2.2,-6 7,-6 l -1,6 6,-2 c 0,5.6 -2.7,8 -6,8 z"/></symbol><symbol id="face-v2" viewBox="0 0 34 34"><path d="M19,31h-5V19h-3v-4h3V8c0-1.8,0.6-2.9,1.4-3.7C16.8,3,19.5,3,19.9,3L23,3v5h-2c-1.1,0-2,0.4-2,2v5h4v4h-4V31z M22,7"/></symbol><symbol id="face" viewBox="0 0 13.4 29.4"><path d="M 8.1,10.508657 V 6.0086565 c 0,0 0,-1.8 1.9,-1.8 h 2.8 V 0.00865651 H 9.3 c 0,0 -5.9,-0.5 -5.9,6.09999999 0,1.4 0,4.5000005 0,4.5000005 H 0 v 3.4 h 3.4 v 15.4 h 4.7 v -15.4 h 4.4 l 0.9,-3.5 H 8.1 z"/></symbol><symbol id="facebook-logo" viewBox="0 0 28 28"><path fill="inherit" d="m 18.339322,13.572791 h -3.27 v 9.42 h -3.52 v -9.43 H 9.0493224 v -2.13 H 11.559322 V 8.7027914 c 0,-4 4.42,-3.69 4.42,-3.69 h 2.6 v 2.57 h -2.1 a 1.17,1.17 0 0 0 -1.41,1.06 v 2.7899996 h 4 z"></path></symbol><symbol id="gallery" viewBox="0 0 20 15"><path d="M 18.1,1.7999998 H 13.9 L 13.5,0.89999981 C 13.2,0.39999981 12.6,-1.9073486e-7 12.1,-1.9073486e-7 H 7.7 C 7.2,-1.9073486e-7 6.6,0.29999981 6.4,0.89999981 L 6,1.7999998 H 1.8 c -1.2,0 -1.8,0.8 -1.8,1.9 V 13.3 c 0,1 0.6,1.8 1.8,1.8 h 16.4 c 1.1,0 1.8,-0.8 1.8,-1.8 V 3.5999998 c -0.1,-1.2 -0.8,-1.8 -1.9,-1.8 z M 10,13.2 c -3,0 -5.5,-2.4 -5.5,-5.3000002 0.1,-2.9 2.5,-5.3 5.5,-5.3 3,0 5.5,2.4 5.5,5.3 C 15.4,10.9 12.9,13.2 10,13.2 z m 0,-8.8000002 c -2,0 -3.7,1.6 -3.7,3.6 0,2 1.7,3.6000002 3.7,3.6000002 2,0 3.7,-1.6000002 3.7,-3.6000002 0,-2 -1.7,-3.6 -3.7,-3.6 z"/></symbol><symbol id="google-play" viewBox="0 0 135 40"><g><path d="m130 40h-125c-2.8 0-5-2.2-5-5v-30c0-2.7 2.2-5 5-5h125c2.8 0 5 2.2 5 5v30c0 2.7-2.3 5-5 5z" class="store-bkg"/><path class="store-frame" d="m130 0.8c2.3 0 4.2 1.9 4.2 4.2v30c0 2.3-1.9 4.2-4.2 4.2h-125c-2.3 0-4.2-1.9-4.2-4.2v-30c0-2.3 1.9-4.2 4.2-4.2h125m0-0.8h-125c-2.8 0-5 2.3-5 5v30c0 2.8 2.2 5 5 5h125c2.8 0 5-2.2 5-5v-30c0-2.7-2.3-5-5-5z" fill="#A6A6A6"/></g><g><path d="m68.1 21.8c-2.4 0-4.3 1.8-4.3 4.3 0 2.4 1.9 4.3 4.3 4.3s4.3-1.8 4.3-4.3c0-2.6-2-4.3-4.3-4.3zm0 6.8c-1.3 0-2.4-1.1-2.4-2.6s1.1-2.6 2.4-2.6 2.4 1 2.4 2.6c0 1.5-1.1 2.6-2.4 2.6zm-9.3-6.8c-2.4 0-4.3 1.8-4.3 4.3 0 2.4 1.9 4.3 4.3 4.3s4.3-1.8 4.3-4.3c0-2.6-2-4.3-4.3-4.3zm0 6.8c-1.3 0-2.4-1.1-2.4-2.6s1.1-2.6 2.4-2.6 2.4 1 2.4 2.6c0 1.5-1.1 2.6-2.4 2.6zm-11.1-5.5v1.8h4.3c-0.1 1-0.5 1.8-1 2.3-0.6 0.6-1.6 1.3-3.3 1.3-2.7 0-4.7-2.1-4.7-4.8s2.1-4.8 4.7-4.8c1.4 0 2.5 0.6 3.3 1.3l1.3-1.3c-1.1-1-2.5-1.8-4.5-1.8-3.6 0-6.7 3-6.7 6.6s3.1 6.6 6.7 6.6c2 0 3.4-0.6 4.6-1.9 1.2-1.2 1.6-2.9 1.6-4.2 0-0.4 0-0.8-0.1-1.1h-6.2zm45.3 1.4c-0.4-1-1.4-2.7-3.6-2.7s-4 1.7-4 4.3c0 2.4 1.8 4.3 4.2 4.3 1.9 0 3.1-1.2 3.5-1.9l-1.4-1c-0.5 0.7-1.1 1.2-2.1 1.2s-1.6-0.4-2.1-1.3l5.7-2.4-0.2-0.5zm-5.8 1.4c0-1.6 1.3-2.5 2.2-2.5 0.7 0 1.4 0.4 1.6 0.9l-3.8 1.6zm-4.6 4.1h1.9v-12.5h-1.9v12.5zm-3.1-7.3c-0.5-0.5-1.3-1-2.3-1-2.1 0-4.1 1.9-4.1 4.3s1.9 4.2 4.1 4.2c1 0 1.8-0.5 2.2-1h0.1v0.6c0 1.6-0.9 2.5-2.3 2.5-1.1 0-1.9-0.8-2.1-1.5l-1.6 0.7c0.5 1.1 1.7 2.5 3.8 2.5 2.2 0 4-1.3 4-4.4v-7.6h-1.8v0.7zm-2.1 5.9c-1.3 0-2.4-1.1-2.4-2.6s1.1-2.6 2.4-2.6 2.3 1.1 2.3 2.6-1 2.6-2.3 2.6zm24.4-11.1h-4.5v12.5h1.9v-4.7h2.6c2.1 0 4.1-1.5 4.1-3.9s-2.1-3.9-4.1-3.9zm0 6h-2.7v-4.3h2.7c1.4 0 2.2 1.2 2.2 2.1 0 1.1-0.8 2.2-2.2 2.2zm11.5-1.8c-1.4 0-2.8 0.6-3.3 1.9l1.7 0.7c0.4-0.7 1-0.9 1.7-0.9 1 0 1.9 0.6 2 1.6v0.1c-0.3-0.2-1.1-0.5-1.9-0.5-1.8 0-3.6 1-3.6 2.8 0 1.7 1.5 2.8 3.1 2.8 1.3 0 1.9-0.6 2.4-1.2h0.1v1h1.8v-4.8c-0.2-2.2-1.8-3.5-4-3.5zm-0.2 6.9c-0.6 0-1.5-0.3-1.5-1.1 0-1 1.1-1.3 2-1.3 0.8 0 1.2 0.2 1.7 0.4-0.1 1.2-1.1 2-2.2 2zm10.6-6.6l-2.1 5.4h-0.1l-2.2-5.4h-2l3.3 7.6-1.9 4.2h1.9l5.1-11.8h-2zm-16.8 8h1.9v-12.5h-1.9v12.5z" fill="#fff"/></g><g><path d="m10.4 7.5c-0.3 0.3-0.5 0.8-0.5 1.4v22.1c0 0.6 0.2 1.1 0.5 1.4l0.1 0.1 12.4-12.4v-0.2l-12.5-12.4z" fill="#00d3ff"/><path d="m27 24.3l-4.1-4.1v-0.3l4.1-4.1 0.1 0.1 4.9 2.8c1.4 0.8 1.4 2.1 0 2.9l-5 2.7z" fill="#ffb500"/><path d="m27.1 24.2l-4.2-4.2-12.5 12.5c0.5 0.5 1.2 0.5 2.1 0.1l14.6-8.4" fill="#f73448"/><path d="m27.1 15.8l-14.6-8.3c-0.9-0.5-1.6-0.4-2.1 0.1l12.5 12.4 4.2-4.2z" fill="#00f076"/></g><g fill="#fff"><path d="m47.9 8.8c0 0.6-0.2 1.1-0.5 1.4-0.5 0.5-1.1 0.7-2 0.7h-0.6v2.5h-1v-6.4c0.5-0.1 1.1-0.1 1.7-0.1 0.8 0 1.5 0.2 1.9 0.5 0.3 0.3 0.5 0.8 0.5 1.4zm-1.1 0.1c0-0.4-0.1-0.7-0.4-0.9-0.2-0.2-0.6-0.3-1-0.3-0.3 0-0.5 0-0.7 0.1v2.3c0.1 0 0.4 0.1 0.6 0.1 0.5 0 0.8-0.1 1.1-0.3s0.4-0.6 0.4-1z"/><path d="m53.7 11c0 0.7-0.2 1.3-0.6 1.8s-1 0.7-1.7 0.7-1.2-0.2-1.7-0.7c-0.4-0.5-0.6-1-0.6-1.7s0.2-1.3 0.6-1.8 1-0.7 1.7-0.7c0.7 0 1.2 0.2 1.7 0.7 0.4 0.5 0.6 1 0.6 1.7zm-1.1 0.1c0-0.4-0.1-0.8-0.3-1.1-0.2-0.4-0.5-0.6-0.9-0.6s-0.7 0.2-1 0.6c-0.2 0.3-0.3 0.7-0.3 1.1s0.1 0.8 0.3 1.1c0.2 0.4 0.5 0.6 1 0.6 0.4 0 0.7-0.2 0.9-0.6 0.2-0.3 0.3-0.7 0.3-1.1z"/><path d="m59.8 11c0 0.8-0.2 1.4-0.7 1.9-0.4 0.4-0.9 0.6-1.4 0.6-0.7 0-1.2-0.3-1.5-0.8l-0.1 0.7h-0.9v-1.3-5.5h1v2.9c0.3-0.5 0.8-0.8 1.5-0.8 0.6 0 1 0.2 1.4 0.7 0.5 0.3 0.7 0.9 0.7 1.6zm-1.1 0.1c0-0.5-0.1-0.8-0.3-1.1s-0.5-0.5-0.9-0.5c-0.3 0-0.5 0.1-0.7 0.3s-0.3 0.4-0.4 0.7v0.3 0.8c0 0.3 0.1 0.6 0.3 0.8s0.5 0.3 0.8 0.3c0.4 0 0.7-0.1 0.9-0.4s0.3-0.7 0.3-1.2z"/><path d="m62.5 7.3c0 0.2-0.1 0.3-0.2 0.5-0.1 0.1-0.3 0.2-0.5 0.2s-0.3-0.1-0.4-0.2-0.2-0.3-0.2-0.5 0.1-0.3 0.2-0.4 0.3-0.2 0.5-0.2c0.2 0 0.3 0.1 0.5 0.2 0 0.1 0.1 0.2 0.1 0.4zm-0.1 6.1h-1v-4.7h1v4.7z"/><path d="m68.1 10.9v0.5h-3.1c0 0.5 0.2 0.8 0.5 1.1 0.3 0.2 0.6 0.3 1 0.3 0.5 0 0.9-0.1 1.3-0.2l0.2 0.7c-0.4 0.2-1 0.3-1.6 0.3-0.7 0-1.3-0.2-1.7-0.6s-0.6-1-0.6-1.7c0-0.7 0.2-1.3 0.6-1.8s1-0.8 1.6-0.8c0.7 0 1.2 0.3 1.5 0.8 0.2 0.3 0.3 0.8 0.3 1.4zm-1-0.3c0-0.3-0.1-0.6-0.2-0.8-0.2-0.3-0.5-0.4-0.8-0.4s-0.6 0.1-0.8 0.4c-0.2 0.2-0.3 0.5-0.3 0.8h2.1z"/><path d="m72.1 9.7h-0.3c-0.4 0-0.7 0.1-0.9 0.4-0.2 0.2-0.3 0.6-0.3 0.9v2.5h-1v-3.2-1.5h0.9v0.9c0.1-0.3 0.3-0.6 0.5-0.7 0.2-0.2 0.5-0.3 0.8-0.3h0.3v1z"/><path d="m76.8 13.4h-3.8v-0.6l1.9-2.5c0.1-0.2 0.3-0.4 0.6-0.8h-2.3v-0.8h3.6v0.6l-1.8 2.4c-0.2 0.3-0.4 0.5-0.6 0.8h2.5l-0.1 0.9z"/><path d="m87.4 8.7l-1.5 4.7h-1l-0.6-2c-0.2-0.5-0.3-1-0.4-1.5-0.1 0.5-0.2 1-0.4 1.5l-0.6 2h-1l-1.4-4.7h1.1l0.5 2.2 0.3 1.5c0.1-0.4 0.2-0.9 0.4-1.5l0.7-2.2h0.9l0.6 2.2c0.2 0.5 0.3 1.1 0.4 1.6 0.1-0.5 0.2-1 0.3-1.6l0.6-2.2h1.1z"/></g></symbol><symbol id="google-plus" viewBox="0 0 28 18"><path d="M385.909,2756a9,9,0,0,0,0,18c5.142,0,8.553-3.65,8.553-8.79a8.417,8.417,0,0,0-.14-1.5h-8.413v3.09h5.053a4.827,4.827,0,0,1-5.053,3.88,5.682,5.682,0,0,1,0-11.36,5,5,0,0,1,3.551,1.38l2.418-2.35a8.515,8.515,0,0,0-5.969-2.35m16.546,5.14h-2.546v2.57h-2.545v2.57h2.545v2.58h2.546v-2.58H405v-2.57h-2.545v-2.57" transform="translate(-377 -2756)"/></symbol><symbol id="gplus-logo" viewBox="0 0 28 28"><path fill="inherit" d="m 24,14.86 h -1.82 v 1.71 h -1.82 v -1.71 h -1.81 v -1.72 h 1.82 v -1.71 h 1.82 v 1.71 H 24 v 1.71 z M 10.36,20 A 6.18,6.18 0 0 1 4,14 6.18,6.18 0 0 1 10.36,8 6.31,6.31 0 0 1 14.62,9.57 L 12.9,11.14 A 3.7,3.7 0 0 0 10.36,10.21 3.87,3.87 0 0 0 6.42,14 3.87,3.87 0 0 0 10.37,17.79 3.39,3.39 0 0 0 14,15.2 h -3.64 v -2.06 h 6 a 5.23,5.23 0 0 1 0.1,1 C 16.47,17.57 14,20 10.36,20 Z"/></symbol><symbol id="guidebook" viewBox="0 0 20.1 20"><path d="m13.2 9.5c-0.8-0.8-2.5-1.8-2.5-1.8s0.4-2.9 0-2.9-2.2 1.4-2.2 1.4c-1.7-0.7-2.9-0.7-2.9-0.7s0 1.2 0.7 2.9c0 0-1.4 1.8-1.4 2.2s2.9 0 2.9 0 1 1.7 1.8 2.5c0 0 0.7-2.9 0.4-3.3 0.4 0.5 3.2-0.3 3.2-0.3zm6.6 8.7-7.3-7.3c-0.4-0.4-1.1-0.4-1.5 0s-0.4 1.1 0 1.5l7.3 7.3c0.4 0.4 1.1 0.4 1.5 0s0.4-1.1 0-1.5zm-12.4-14.9s0.6 1 1 1.5c0 0 0.4-1.7 0.2-1.9 0.2 0.2 1.9-0.2 1.9-0.2-0.5-0.5-1.5-1-1.5-1s0.3-1.7 0-1.7c-0.2 0-1.2 0.8-1.2 0.8-1-0.4-1.7-0.4-1.7-0.4s0 0.7 0.4 1.7c0 0-0.8 1-0.8 1.2 0 0.3 1.7 0 1.7 0zm7.2 3.2s0.6 1 1 1.5c0 0 0.4-1.7 0.2-1.9 0.2 0.2 1.9-0.2 1.9-0.2-0.5-0.5-1.5-1-1.5-1s0.2-1.7 0-1.7-1.3 0.8-1.3 0.8c-1-0.4-1.7-0.4-1.7-0.4s0 0.7 0.4 1.7c0 0-0.8 1-0.8 1.3 0.1 0.2 1.8-0.1 1.8-0.1zm-7.2 7.1c-0.2 0-1.3 0.8-1.3 0.8-0.9-0.4-1.6-0.4-1.6-0.4s0 0.7 0.4 1.7c0 0-0.8 1-0.8 1.3 0 0.2 1.7 0 1.7 0s0.6 1 1 1.5c0 0 0.4-1.7 0.2-1.9 0.2 0.2 1.9-0.2 1.9-0.2-0.5-0.5-1.5-1-1.5-1s0.3-1.8 0-1.8zm-2.5-6.1c-0.5-0.5-1.5-1-1.5-1s0.2-1.7 0-1.7-1.3 0.8-1.3 0.8c-1-0.4-1.7-0.4-1.7-0.4s0 0.7 0.4 1.7c0 0-0.8 1-0.8 1.3 0 0.2 1.7 0 1.7 0s0.6 1 1 1.5c0 0 0.4-1.7 0.2-1.9 0.3 0.2 2-0.3 2-0.3z"/></symbol><symbol id="hamburger" viewBox="0 0 40 34"><path d="M20,32H60v6H20V32Zm0,14H60v6H20V46Zm0,14H60v6H20V60Z" transform="translate(-20 -32)"/></symbol><symbol id="infographic" viewBox="0 0 13 17"><path d="M 0,17 H 3 V 0 H 0 v 17 z m 5,0 H 8 V 8 H 5 v 9 z M 10,3 v 14 h 3 V 3 h -3 z"/></symbol><symbol id="instagram-v2" viewBox="0 0 30 30"><path d="M 15 3 C 11.75 3 11.340547 2.9996094 10.060547 3.0996094 A 8.73 8.73 0 0 0 7.1601562 3.6601562 A 5.53 5.53 0 0 0 5 5 A 5.77 5.77 0 0 0 3.6308594 7.1601562 A 8.73 8.73 0 0 0 3.0703125 10.060547 C 3.0003125 11.340547 3 11.75 3 15 C 3 18.25 2.9996094 18.660703 3.0996094 19.970703 A 8.73 8.73 0 0 0 3.6601562 22.869141 A 6.11 6.11 0 0 0 7.1601562 26.369141 A 8.73 8.73 0 0 0 10.060547 26.929688 C 11.340547 26.999688 11.75 27 15 27 C 18.25 27 18.659453 27.000391 19.939453 26.900391 A 8.73 8.73 0 0 0 22.839844 26.339844 A 6.1 6.1 0 0 0 26.339844 22.839844 A 8.73 8.73 0 0 0 26.900391 19.939453 C 27.000391 18.659453 27 18.25 27 15 C 27 11.75 27.000391 11.339297 26.900391 10.029297 A 8.73 8.73 0 0 0 26.339844 7.1308594 A 6.11 6.11 0 0 0 22.839844 3.6308594 A 8.73 8.73 0 0 0 19.939453 3.0703125 C 18.659453 3.0003125 18.25 3 15 3 z M 15 5.1699219 C 18.2 5.1699219 18.579844 5.2402344 19.839844 5.2402344 A 6.48 6.48 0 0 1 22.060547 5.6601562 A 3.69 3.69 0 0 1 23.439453 6.5507812 A 3.84 3.84 0 0 1 24.339844 7.9296875 A 6.71 6.71 0 0 1 24.75 10.150391 C 24.82 11.420391 24.820312 11.790234 24.820312 14.990234 C 24.820312 18.190234 24.8 18.570078 24.75 19.830078 A 6.71 6.71 0 0 1 24.339844 22.050781 A 4 4 0 0 1 22.060547 24.330078 A 6.71 6.71 0 0 1 19.839844 24.740234 C 18.579844 24.810234 18.2 24.810547 15 24.810547 C 11.8 24.810547 11.430156 24.790234 10.160156 24.740234 A 6.71 6.71 0 0 1 7.9394531 24.330078 A 3.84 3.84 0 0 1 6.5605469 23.429688 A 3.69 3.69 0 0 1 5.6699219 22.050781 A 6.48 6.48 0 0 1 5.25 19.830078 C 5.18 18.570078 5.1796875 18.190234 5.1796875 14.990234 C 5.1796875 11.790234 5.25 11.410391 5.25 10.150391 A 6.48 6.48 0 0 1 5.6699219 7.9296875 A 3.69 3.69 0 0 1 6.5605469 6.5507812 A 3.84 3.84 0 0 1 7.9394531 5.6503906 A 6.71 6.71 0 0 1 10.160156 5.2402344 C 11.430156 5.1702344 11.8 5.1699219 15 5.1699219 z "/><path d="M 14.744141 8.8554688 A 6.15 6.15 0 0 0 15 21.150391 A 6.15 6.15 0 0 0 21.150391 15 A 6.15 6.15 0 0 0 14.744141 8.8554688 z M 15 11 A 4 4 0 0 1 19 15 A 4 4 0 1 1 15 11 z "/><path d="m 21.39,10.05 a 1.44,1.44 0 1 1 1.44,-1.44 1.43,1.43 0 0 1 -1.44,1.44 z"/></symbol><symbol id="instagram" viewBox="0 0 19.8 19.4"><path class="st0" d="M17.3,0H2.6C1.2,0,0,1.1,0,2.5v14.4c0,1.4,1.2,2.5,2.5,2.5h14.7c1.4,0,2.5-1.1,2.5-2.5V2.5 C19.8,1.1,18.7,0,17.3,0z M14.2,3C14.2,3,14.2,3,14.2,3c0-0.4,0.3-0.7,0.8-0.7h1.8c0.4,0,0.7,0.3,0.8,0.7v1.8c0,0.4-0.3,0.7-0.8,0.7 h-1.8c0,0,0,0,0,0c-0.4,0-0.7-0.3-0.7-0.7V3z M9.9,5.5L9.9,5.5L9.9,5.5L9.9,5.5c2.3,0,4.1,1.9,4.1,4.2s-1.9,4.1-4.2,4.1 c-2.3,0-4.1-1.9-4.1-4.2C5.7,7.4,7.6,5.5,9.9,5.5z M17.9,16.7c0,0.4-0.4,0.8-0.8,0.8H2.7c-0.4,0-0.8-0.3-0.8-0.8V8.3h2 C3.8,8.7,3.8,9.2,3.8,9.7c0.1,3.3,2.7,6,6,6c3.4,0.1,6.2-2.6,6.3-6c0-0.5-0.1-0.9-0.2-1.4h2.1h0V16.7z"/></symbol><symbol id="interview-2" viewBox="0 0 30 30"><path d="m9.01,18.9a18.312,18.312,0,0,1-5.04,2.04c-1.05.21-.73-0.08-0.47-0.29a9.884,9.884,0,0,0,2.74-2.87,5.366,5.366,0,0,1-2.97-4.43c0-3.27,3.95-5.92,8.82-5.92s8.82,2.65,8.82,5.92-3.95,5.92-8.82,5.92Z m17.42,3.64a10.81,10.81,0,0,1-3.28-1.5,8.077,8.077,0,0,1-2.42.37,6.77,6.77,0,0,1-4.8-1.75c3.33-.97,5.66-3.2,5.88-5.89,2.66,0.34,4.66,1.9,4.66,3.78a3.317,3.317,0,0,1-1.52,2.62c0.06,0.44.26,1.04,1.61,2.1Z"/></symbol><symbol id="interview" viewBox="0 0 20 16"><path d="m 13.3,10.4 c 1.2,0 2.2,-0.9 2.2,-2.1 V 2.1 C 15.5,1 14.5,0 13.3,0 H 2.2 C 1,0 0,0.9 0,2.1 v 6.2 c 0,1.1 1,2.1 2.2,2.1 H 3.6 L 4.5,13.9 9,10.4 h 4.3 z m 4.5,-7.6 h -1.4 l -0.1,7 c 0,1.3 -1.5,1.3 -1.5,1.3 H 9.4 L 8.1,12.5 H 11 l 4.4,3.5 0.9,-3.5 h 1.4 c 1.3,0 2.3,-1 2.3,-2.1 V 4.8 c 0,-1.1 -1,-2 -2.2,-2 z"/></symbol><symbol id="left-arrow" viewBox="0 0 19.200001 32"><path d="M17.13 32l2.07-1.93L4.15 16 19.2 1.94 17.13 0 0 16"/></symbol><symbol id="letter" viewBox="0 0 29 21"><path d="M 0,18.6 C 0,19.9 1.1,21 2.4,21 H 26.6 C 27.9,21 29,19.9 29,18.6 V 2.4 C 29,1.1 27.9,0 26.6,0 H 2.4 C 1.1,0 0,1.1 0,2.4 V 18.6 z M 8.9,10.5 3.4,5 C 2.9,4.5 2.9,3.8 3.4,3.4 3.9,2.9 4.6,2.9 5.1,3.4 l 8.8,8.8 c 0.4,0.3 0.9,0.3 1.3,0 L 24,3.4 c 0.5,-0.5 1.2,-0.5 1.7,0 0.5,0.5 0.5,1.2 0,1.6 l -5.6,5.5 5.5,5.5 c 0.5,0.5 0.5,1.2 0,1.6 -0.5,0.5 -1.2,0.5 -1.7,0 l -5.5,-5.5 c 0,0 -1.7,1.7 -2,2.1 -0.5,0.5 -1.2,0.8 -1.9,0.8 -0.7,0 -1.4,-0.3 -1.9,-0.8 -0.3,-0.3 -2,-2 -2,-2 l -5.5,5.5 c -0.5,0.5 -1.2,0.5 -1.7,0 -0.5,-0.5 -0.5,-1.2 0,-1.6 l 5.5,-5.6 z"/></symbol><symbol id="linkedin" viewBox="0 0 30 30"><path d="m8.89,25.56h-4.45v-14.31h4.45v14.31Zm-2.22-16.27h0a2.58,2.58,0,1,1,2.57-2.58Zm14.44,16.27v-6.96c0-1.66-.03-3.79-2.31-3.79s-2.69,1.81-2.69,3.67v7.08h-4.44v-14.31h4.26v1.96h0.06a4.7,4.7,0,0,1,4.22-2.32c4.5,0,5.33,2.97,5.33,6.83l0.02,7.84h-4.45Z"/></symbol><symbol id="magnifier" viewBox="0 0 28.1875 28.19375"><path d="M27.944 26.756l-6.852-6.852c1.838-2.113 2.95-4.87 2.95-7.883C24.043 5.39 18.65 0 12.023 0 5.388 0 0 5.395 0 12.02c0 6.628 5.395 12.023 12.02 12.023 3.015 0 5.77-1.113 7.884-2.95l6.852 6.85c.162.163.38.25.594.25.212 0 .43-.08.594-.25.325-.324.325-.862 0-1.187zM1.682 12.02c0-5.7 4.638-10.332 10.333-10.332 5.7 0 10.334 4.638 10.334 10.333 0 5.696-4.634 10.34-10.335 10.34-5.695 0-10.333-4.637-10.333-10.34z"/></symbol><symbol id="mail-v2" viewBox="0 0 34 34"><polygon points="5,9 17,20 29,9 29,25 5,25"/><polygon points="17,18.5 27.5,9 6.5,9"/></symbol><symbol id="newspaper" viewBox="0 0 20 16"><path d="M3894,2213h4v4h-4v-4Zm-5-3h9v2h-9v-2Zm0,3h4v1h-4v-1Zm0,2h4v1h-4v-1Zm11-8h-1v-2a0.979,0.979,0,0,0-1-1h-16a0.979,0.979,0,0,0-1,1v14a0.979,0.979,0,0,0,1,1h18a0.979,0.979,0,0,0,1-1v-11A0.979,0.979,0,0,0,3900,2207Zm-14.5,9v-10H3897v2h-10v10h-4v-12h1.5v10h1Zm13.5-7v9h-11v-9h11Z" transform="translate(-3881 -2204)"/></symbol><symbol id="obituaries" viewBox="0 0 32 8.4407187"><path d="M4.465 3.478c-1.342 0-2.712.94-2.712 2.108 0 .43.154.91.575 1.197.21.154.21.393-.03.652-.286.297-.698.536-1.12.536C.67 7.97 0 7.51 0 6.62c0-.498.335-1.552 1.648-2.52 1.39-1.025 2.577-1.37 3.938-1.456C8.46.402 10.788 0 12.484 0c1.85 0 3.497.575 4.33.89.748.28 3.65 1.467 5.7 2.34 2.157.87 4.015 1.398 5.29 1.398 1.015 0 2.49-.355 2.49-1.58 0-.452-.18-.758-.44-.94-.144-.115-.25-.288-.25-.44 0-.45.413-1.14 1.14-1.14C31.56.527 32 1.32 32 1.925c0 1.628-1.83 2.73-3.056 3.228-2.26 2.348-4.55 3.287-6.764 3.287-1.63 0-3.19-.43-5.26-1.16-1.562-.554-6.458-2.51-8.422-3.112-1.81-.546-2.845-.69-4.014-.69h-.02zM10.778.88c-1.504 0-2.93.74-3.63 1.83.852.02 2.145.203 4.292.96 2.146.708 6.103 2.356 7.482 2.826 1.543.488 3.01.9 4.474.9 1.64 0 3.095-.527 3.86-1.877-1.522.19-2.96-.05-5.076-.854-1.63-.604-6.113-2.616-7.627-3.114-1.274-.42-2.51-.67-3.746-.67h-.03z"/></symbol><symbol id="padlock-close" viewBox="0 0 28.1 37.5"><path d="M28.1,17H0v20.5h28.1V17z M15.8,28.1l0,5.9l-3.6,0v-5.9c-1.1-0.6-1.9-1.8-1.9-3.2c0-2,1.7-3.7,3.7-3.7 c2,0,3.7,1.7,3.7,3.7C17.7,26.3,17,27.5,15.8,28.1z"/><path d="M7.8,15.1c0-0.3-0.1-3.6,0.1-4.9c0.3-2.9,3.1-5.3,6.2-5.3c2.9,0,5.5,2.2,6.1,5.2c0.3,1.2,0.1,4.7,0.1,5h4.9 c0-0.3,0.1-5.1-0.5-7.1C23.4,3.4,19,0.1,14.1,0C9.4-0.1,5.2,2.9,3.5,7.5c-0.8,2.1-0.6,7.3-0.6,7.6H7.8z"/></symbol><symbol id="padlock-open" viewBox="0 0 42.6 37.5"><path d="M42.6,17H14.5v20.5h28.1V17z M30.3,28.1V34h-3.6v-5.9c-1.1-0.6-1.9-1.8-1.9-3.2c0-2,1.7-3.7,3.7-3.7 s3.7,1.7,3.7,3.7C32.2,26.3,31.5,27.5,30.3,28.1z"/><path d="M4.9,15.1c0-0.3-0.1-3.6,0.1-4.9c0.3-2.9,3.1-5.3,6.2-5.3c2.9,0,5.5,2.2,6.1,5.2c0.3,1.2,0.1,4.7,0.1,5h4.9 c0-0.3,0.1-5.1-0.5-7.1c-1.3-4.6-5.7-7.9-10.6-8C6.5-0.1,2.3,2.9,0.6,7.5C-0.2,9.6,0,14.8,0,15.1H4.9z"/></symbol><symbol id="padlock" viewBox="0 0 45 56"><path d="M 38,21 H 36.3 V 14.3 C 36.3,6.3 30.6,0 22.4,0 14.2,0 8.6,6.4 8.6,14.3 V 21 H 6.9 C 3.1,21 0,24.1 0,28 v 21 c 0,3.9 3.1,7 6.9,7 H 38.1 C 41.9,56 45,52.9 45,49 V 28 C 44.9,24.1 41.8,21 38,21 z M 12,14.3 c 0,-6 4,-10.8 10.3,-10.8 6.2,0 10.4,4.8 10.4,10.8 V 21 H 12 V 14.3 z M 41.4,49 c 0,1.9 -1.6,3.5 -3.5,3.5 H 6.8 C 4.9,52.5 3.3,50.9 3.3,49 V 28 c 0,-1.9 1.6,-3.5 3.5,-3.5 H 38 c 1.9,0 3.5,1.6 3.5,3.5 v 21 h -0.1 z m -19,-17.5 c -1.9,0 -3.5,1.6 -3.5,3.5 0,1.3 0.7,2.4 1.7,3 v 5.7 c 0,1 0.8,1.7 1.7,1.7 1,0 1.7,-0.8 1.7,-1.7 V 38 c 1,-0.6 1.7,-1.7 1.7,-3 0.2,-1.9 -1.4,-3.5 -3.3,-3.5 z"/></symbol><symbol id="post-delete" viewBox="0 0 14 14"><path d="M588.55,437a7,7,0,1,1,7-7A7,7,0,0,1,588.55,437Zm0-13a6,6,0,1,0,6,6A6,6,0,0,0,588.55,424Z" transform="translate(-581.55 -423)"/><polygon points="10.31 4.66 9.35 3.69 7 6.03 4.66 3.69 3.69 4.66 6.04 7 3.69 9.34 4.66 10.31 7 7.96 9.35 10.31 10.31 9.34 7.96 7 10.31 4.66"/></symbol><symbol id="questionnaire" viewBox="0 0 20 20"><path d="M2.5,2.5v15h15v-15H2.5z M8.8,13.5l-1.2-1.2L5.1,10l1.2-1.2l2.4,2.4l4.9-4.7l1.2,1.2L8.8,13.5z"/></symbol><symbol id="quiz" viewBox="0 0 15 19.5"><path d="m6.9 3.3c-0.1-0.2-0.1-0.5-0.1-0.7 0-0.3 0.1-0.5 0.2-0.6 0.1-0.2 0.3-0.3 0.5-0.3s0.4 0.1 0.5 0.2 0.2 0.3 0.2 0.6-0.1 0.6-0.2 0.9-0.3 0.6-0.5 0.8c-0.2 0.3-0.4 0.5-0.6 0.8-0.3 0.2-0.5 0.4-0.8 0.5v2.2h1.9v-1.5c0.6-0.4 1.1-0.9 1.5-1.5s0.5-1.3 0.5-2.1-0.2-1.5-0.7-1.9c-0.4-0.5-1-0.7-1.9-0.7-0.8 0-1.4 0.2-1.9 0.7-0.4 0.5-0.6 1-0.6 1.8 0 0.4 0.1 0.8 0.2 1.2l1.8-0.4zm7.4 3.4c-0.5-0.4-1.1-0.7-2-0.7-0.8 0-1.5 0.2-1.9 0.7s-0.6 1-0.6 1.8c0 0.4 0.1 0.8 0.2 1.2l1.8-0.3c-0.1-0.2-0.1-0.5-0.1-0.7 0-0.3 0.1-0.5 0.2-0.7s0.3-0.3 0.5-0.3 0.4 0.1 0.5 0.2 0.2 0.3 0.2 0.6-0.1 0.6-0.2 0.9-0.3 0.6-0.5 0.9-0.4 0.5-0.6 0.7-0.5 0.4-0.7 0.6v2.2h1.9v-1.6c0.6-0.4 1.1-0.9 1.5-1.5s0.5-1.3 0.5-2.1-0.3-1.4-0.7-1.9zm-3.3 9.9h1.9v-1.9h-1.9v1.9zm-3.6-7.6c-0.8 0-1.5 0.2-1.9 0.7-0.4 0.4-0.6 1-0.6 1.8 0 0.4 0.1 0.8 0.2 1.2l1.8-0.3c-0.1-0.2-0.1-0.5-0.1-0.7 0-0.3 0.1-0.5 0.2-0.7s0.3-0.3 0.5-0.3 0.4 0.1 0.5 0.2 0.2 0.3 0.2 0.6-0.1 0.6-0.2 0.9-0.3 0.6-0.5 0.9-0.4 0.5-0.6 0.7-0.5 0.4-0.7 0.6v2.2h1.9v-1.6c0.6-0.4 1.1-0.9 1.5-1.5s0.5-1.3 0.5-2.1-0.2-1.5-0.7-1.9c-0.5-0.5-1.2-0.7-2-0.7zm-1.3 10.5h1.9v-1.9h-1.9v1.9zm-4.9-3.9h1.9v-1.9h-1.9v1.9zm3.3-9.9c-0.5-0.4-1.1-0.6-2-0.6-0.8 0-1.5 0.2-1.9 0.7-0.4 0.4-0.6 1-0.6 1.8 0 0.4 0.1 0.8 0.2 1.2l1.8-0.3c-0.1-0.4-0.1-0.6-0.1-0.8 0-0.3 0.1-0.5 0.2-0.7s0.2-0.3 0.4-0.3 0.4 0.1 0.5 0.2 0.2 0.3 0.2 0.6-0.1 0.6-0.2 0.9c0 0.4-0.2 0.6-0.4 0.9s-0.4 0.5-0.6 0.7-0.5 0.4-0.7 0.6v2.2h1.9v-1.6c0.6-0.4 1.1-0.9 1.5-1.5s0.5-1.3 0.5-2.1-0.2-1.4-0.7-1.9z"/></symbol><symbol id="read-later" viewBox="0 0 34 34"><path d="M26,31l-9-8l-9,8V3h18V31z"/></symbol><symbol id="reply" viewBox="0 0 17 9"><path d="M0,4.5L5.7,0v2.2C15.9,2.2,17,7.9,17,9C14.7,4.5,6.8,6.8,5.7,6.8V9L0,4.5z"/></symbol><symbol id="right-arrow" viewBox="0 0 19.200001 32"><path d="M15.05 16L0 1.94 2.07 0 19.2 16 2.07 32 0 30.07"/></symbol><symbol id="snapchat" viewBox="0 0 21 20"><path d="M570.633,2775a0.86,0.86,0,0,1-.161-0.01h0c-0.034,0-.069.01-0.105,0.01a4.524,4.524,0,0,1-2.814-1.13,4.123,4.123,0,0,0-1.63-.84,4.9,4.9,0,0,0-.855-0.08,5.92,5.92,0,0,0-1.186.14,2.191,2.191,0,0,1-.441.07,0.282,0.282,0,0,1-.307-0.23c-0.049-.16-0.084-0.33-0.119-0.49a0.8,0.8,0,0,0-.32-0.69c-1.973-.31-2.538-0.73-2.664-1.02a0.479,0.479,0,0,1-.03-0.13,0.227,0.227,0,0,1,.186-0.24c3.033-.5,4.393-3.65,4.45-3.78l0-.02a1.175,1.175,0,0,0,.109-0.98,2.024,2.024,0,0,0-1.337-.86c-0.11-.04-0.214-0.07-0.3-0.1-0.9-.37-0.971-0.73-0.936-0.92a0.851,0.851,0,0,1,.824-0.55,0.531,0.531,0,0,1,.245.06,2.582,2.582,0,0,0,1.081.28,0.853,0.853,0,0,0,.645-0.21c-0.01-.2-0.024-0.42-0.038-0.65a10.884,10.884,0,0,1,.251-4.29,5.473,5.473,0,0,1,5.1-3.34h0.427a5.483,5.483,0,0,1,5.1,3.34,10.9,10.9,0,0,1,.251,4.29l0,0.06c-0.012.21-.024,0.4-0.034,0.59a0.891,0.891,0,0,0,.588.21h0a2.644,2.644,0,0,0,1.019-.29,0.853,0.853,0,0,1,.316-0.06,0.908,0.908,0,0,1,.364.07h0.007a0.682,0.682,0,0,1,.509.56,1.112,1.112,0,0,1-.944.84,1.728,1.728,0,0,1-.3.1,2.1,2.1,0,0,0-1.337.86,1.2,1.2,0,0,0,.109.99,0.042,0.042,0,0,0,0,.01c0.057,0.13,1.416,3.28,4.45,3.79a0.224,0.224,0,0,1,.186.23,0.336,0.336,0,0,1-.031.13c-0.125.3-.689,0.72-2.663,1.03a0.748,0.748,0,0,0-.32.68c-0.035.17-.07,0.33-0.119,0.49a0.266,0.266,0,0,1-.283.22H577.56a2.2,2.2,0,0,1-.442-0.06,6.738,6.738,0,0,0-1.186-.13,6.137,6.137,0,0,0-.856.08,4.093,4.093,0,0,0-1.627.84A4.534,4.534,0,0,1,570.633,2775Z" transform="translate(-560 -2755)"/></symbol><symbol id="spam" viewBox="0 0 11 14.5"><path d="M2.3,14.5h6.5c0.8,0,1.4-0.6,1.4-1.5V2.9H0.8v10.2C0.8,13.9,1.5,14.5,2.3,14.5z M7.7,4.7 c0-0.3,0.2-0.5,0.6-0.5c0.3,0,0.6,0.2,0.6,0.5v7.9c0,0.3-0.3,0.5-0.6,0.5s-0.6-0.2-0.6-0.5C7.7,12.6,7.7,4.7,7.7,4.7z M5,4.7 c0-0.3,0.3-0.5,0.6-0.5c0.3,0,0.6,0.2,0.6,0.5v7.9c0,0.3-0.3,0.5-0.6,0.5c-0.3,0-0.6-0.2-0.6-0.5V4.7z M2.2,4.7 c0-0.3,0.2-0.5,0.6-0.5s0.6,0.2,0.6,0.5v7.9c0,0.3-0.3,0.5-0.6,0.5s-0.6-0.2-0.6-0.5C2.2,12.6,2.2,4.7,2.2,4.7z"/><path d="M3.7,0h-2C0.8,0,0,0.8,0,1.7h11c0-1-0.8-1.7-1.8-1.7H3.7"/></symbol><symbol id="star" viewBox="0 0 15 14"><path d="M500.943,3644.22a1.056,1.056,0,0,0-.865-0.68l-3.9-.56-1.695-3.4a1.12,1.12,0,0,0-1.962,0l-1.7,3.4-3.9.56a1.058,1.058,0,0,0-.864.68,0.971,0.971,0,0,0,.251,1.03l2.861,2.75-0.664,3.82a0.981,0.981,0,0,0,.446.99,1.118,1.118,0,0,0,1.147.06l3.4-1.76,3.4,1.76a1.117,1.117,0,0,0,1.146-.06,0.98,0.98,0,0,0,.444-0.99l-0.663-3.82,2.86-2.75A0.964,0.964,0,0,0,500.943,3644.22Z" transform="translate(-486 -3639)"/></symbol><symbol id="survey" viewBox="0 0 20 20"><path d="m 8.9,2.1 0,0 c -5,0 -8.9,4 -8.9,9 0,4.9 4,9 8.9,9 4.9,0 8.9,-4 8.9,-9 -2.3,0 -8.9,0 -8.9,0 0,0 0,-6.6 0,-9 z M 20,9.6 C 20,4.3 15.7,0 10.4,0 V 9.7 L 20,9.6 z"/></symbol><symbol id="tweet" viewBox="0 0 28 24"><path d="M 28.1,2.8 C 27.1,3.3 25.9,3.6 24.8,3.7 26,3 26.9,1.8 27.4,0.4 26.3,1.1 25,1.6 23.7,1.9 22.6,0.7 21.1,0 19.5,0 c -3.2,0 -5.8,2.7 -5.8,6 0,0.5 0.1,0.9 0.2,1.4 C 9.1,7.1 4.8,4.8 1.9,1.1 1.4,2 1.1,3 1.1,4.1 c 0,2.1 1,3.9 2.6,5 -1,0 -1.9,-0.3 -2.6,-0.8 v 0.1 c 0,2.9 2,5.3 4.7,5.9 -0.5,0.1 -1,0.2 -1.5,0.2 -0.4,0 -0.7,0 -1.1,-0.1 0.7,2.4 2.9,4.1 5.4,4.2 -2,1.6 -4.5,2.6 -7.2,2.6 -0.5,0 -0.9,0 -1.4,-0.1 2.6,1.7 5.6,2.7 8.9,2.7 10.7,0 16.6,-9.2 16.6,-17.1 0,-0.3 0,-0.5 0,-0.8 0.8,-0.8 1.8,-1.9 2.6,-3.1 z"/></symbol><symbol id="twitter-v2" viewBox="0 0 34 34"><path d="M12.9,27.8c0,0-0.1,0-0.1,0c-5,0-8-2.4-8.1-2.5l-1.5-1.3l1.9,0.4c0,0,3,0.6,6.6-1.5C8.2,22,7.4,19,7.4,19
+ l-0.2-0.7l0.7,0.1c0.1,0,0.2,0,0.3,0c-2.2-1.4-2.8-4-2.7-5l0.1-0.7L6.2,13c0.2,0.1,0.5,0.2,0.7,0.3c-1.1-1.3-2.2-3.6-0.7-6.7L6.6,6
+ L7,6.6c3.9,4.4,8.6,5.4,10.2,5.4c-0.4-3.2,2-5.9,4.5-6.5c2.5-0.6,4.5,0.6,5.5,1.5l3.7-1.5c0,0-0.7,1.8-2.1,3.3l2.9-1.1l-0.4,0.9
+ c0,0.1-0.8,1.3-2.2,2.5c0.1,5.6-2.8,10.4-5.3,12.6C20.7,26.5,17.2,27.8,12.9,27.8z"/></symbol><symbol id="up-arrow" viewBox="0 0 21.2 14.1"><rect x="-0.4" y="4.6" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -2.9289 7.0711)" width="15" height="5"/><rect x="6.6" y="4.6" transform="matrix(-0.7071 -0.7071 0.7071 -0.7071 19.1421 22.0711)" width="15" height="5"/></symbol><symbol id="user" viewBox="0 0 32 32"><path d="M8.4,23.9c-0.2,0-0.4-0.1-0.4-0.3c-0.1-0.2,0.1-0.5,0.3-0.5c1.6-0.4,3.9-1.2,4.4-1.7
+ c0.1-0.2,0.1-1,0-1.9c-0.7-0.6-1.2-1.5-1.4-2.4c-0.2-0.9-0.5-2.5-0.6-3.3c-0.1-1-0.1-1.4,0-2.1l0-0.1c0.3-2.2,2.5-3.9,5.2-3.9
+ c2.7,0,4.9,1.7,5.2,3.9l0,0.1c0.1,0.6,0.1,1.1,0,2.1c-0.1,0.8-0.4,2.5-0.6,3.3c-0.2,0.9-0.7,1.7-1.4,2.4c-0.1,0.8-0.1,1.7,0,1.9
+ c0.5,0.5,3,1.3,4.5,1.7c0.2,0.1,0.4,0.3,0.3,0.5s-0.3,0.4-0.5,0.3c-0.7-0.2-4.2-1-5-2c-0.5-0.6-0.3-2.1-0.2-2.7
+ c0-0.1,0.1-0.2,0.1-0.3c0.6-0.5,1-1.2,1.2-2c0.2-0.8,0.5-2.4,0.6-3.2c0.1-0.9,0.1-1.2,0-1.8l0-0.1C20,10,18.2,8.6,15.9,8.6
+ c-2.2,0-4.1,1.3-4.3,3.1l0,0.1c-0.1,0.6-0.1,1,0,1.9c0.1,0.8,0.4,2.4,0.6,3.2c0.2,0.8,0.6,1.5,1.2,2c0.1,0.1,0.1,0.2,0.1,0.3
+ c0.1,0.6,0.3,2.2-0.2,2.7c-0.8,0.9-4.1,1.8-4.8,2C8.5,23.9,8.4,23.9,8.4,23.9z"/><path d="M16,30.3c-7.7,0-13.9-6.2-13.9-13.9C2.1,8.8,8.3,2.5,16,2.5c7.7,0,13.9,6.2,13.9,13.9
+ C29.9,24.1,23.7,30.3,16,30.3z M16,3.4c-7.2,0-13,5.8-13,13s5.8,13,13,13s13-5.8,13-13S23.2,3.4,16,3.4z"/></symbol><symbol id="video-round" viewBox="0 0 20 20"><path d="M10,20A10,10,0,1,1,20,10,10,10,0,0,1,10,20ZM10,.83A9.17,9.17,0,1,0,19.17,10,9.19,9.19,0,0,0,10,.83Z" style="fill:currentColor"/><polygon points="16.17 10 5.83 15.97 5.83 4.06 16.17 10"/></symbol><symbol id="video" viewBox="0 0 14.6 15.85"><path d="M 14.1,7.05 1.5,0.15 c -0.3,-0.2 -0.7,-0.2 -1,0 C 0.2,0.35 0,0.65 0,1.05 v 13.8 c 0,0.4 0.2,0.7 0.5,0.9 0.2,0.1 0.3,0.1 0.5,0.1 0.2,0 0.3,0 0.5,-0.1 l 12.6,-6.9 c 0.3,-0.2 0.5,-0.5 0.5,-0.9 0,-0.4 -0.2,-0.7 -0.5,-0.9 z"/></symbol><symbol id="vote-down" viewBox="0 0 14.44 14.11"><path d="M12.22-.24a0.66,0.66,0,0,0-.61.43v0l-1.45-.07A7.88,7.88,0,0,0,8-.9a13,13,0,0,0-4.42,0S2.1-.84,1.64,1.23c0,0-1.29,4.1.07,5.88,0,0,.62,1.16,3,0.93H5.51s-1.22,3.18.21,4.54c0,0,1.54,1,2.1-.1l0-.1C8.37,11.14,9.34,7.62,11.59,7h3.52s0.41,0,.41-0.6V0.57a0.87,0.87,0,0,0-.76-0.8H12.22Z" transform="translate(-1.09 1.11)"/></symbol><symbol id="vote-up" viewBox="0 0 14.44 14.11"><path d="M4.17,9.17a0.66,0.66,0,0,0,.61-0.43v0L6.24,8.77A7.88,7.88,0,0,0,8.39,9.83a13,13,0,0,0,4.42,0s1.47-.07,1.94-2.14c0,0,1.29-4.1-.07-5.88,0,0-.62-1.16-3-0.93H10.88s1.22-3.18-.21-4.54c0,0-1.54-1-2.1.1l0,0.1C8-2.21,7.05,1.31,4.79,1.89H1.27s-0.41,0-.41.6V8.36a0.87,0.87,0,0,0,.76.8H4.17Z" transform="translate(-0.86 4.06)"/></symbol><symbol id="windows-store" viewBox="0 0 135 40"><g><path class="store-frame" d="m130.2 40h-125.5c-2.6 0-4.7-2.1-4.7-4.7v-30.6c0-2.6 2.1-4.7 4.7-4.7h125.5c2.6 0 4.8 2.1 4.8 4.7v30.5c0 2.7-2.2 4.8-4.8 4.8z" fill="#A6A6A6"/><path d="m134 35.3c0 2.1-1.7 3.8-3.8 3.8h-125.5c-2.1 0-3.8-1.7-3.8-3.8v-30.6c0-2.1 1.7-3.8 3.8-3.8h125.5c2.1 0 3.8 1.7 3.8 3.8v30.6z" class="store-bkg"/></g><g fill="#fff"><path d="m63.5 20.8c0-0.4 0.1-0.7 0.4-1s0.6-0.4 1-0.4 0.8 0.1 1 0.4c0.3 0.3 0.4 0.6 0.4 1s-0.1 0.7-0.4 1-0.6 0.4-1 0.4-0.7-0.1-1-0.4c-0.2-0.3-0.4-0.7-0.4-1m2.6 12.7h-2.3v-9.8h2.3v9.8z"/><path d="m73.1 31.8c0.3 0 0.7-0.1 1.1-0.2l1.2-0.6v2.2c-0.4 0.2-0.8 0.4-1.3 0.5s-1 0.2-1.5 0.2c-1.5 0-2.6-0.5-3.5-1.4s-1.4-2.1-1.4-3.5c0-1.6 0.5-2.9 1.4-3.9s2.2-1.5 3.9-1.5c0.4 0 0.9 0.1 1.3 0.2s0.8 0.2 1.1 0.4v2.2c-0.4-0.3-0.7-0.5-1.1-0.6s-0.8-0.2-1.1-0.2c-0.9 0-1.7 0.3-2.2 0.9s-0.8 1.4-0.8 2.4c0 1 0.3 1.8 0.8 2.3 0.5 0.3 1.2 0.6 2.1 0.6"/><path d="m82 23.5h0.5c0.1 0 0.3 0.1 0.4 0.1v2.3c-0.1-0.1-0.3-0.2-0.5-0.3s-0.5-0.1-0.8-0.1c-0.6 0-1 0.2-1.4 0.7s-0.6 1.2-0.6 2.2v5h-2.3v-9.8h2.3v1.5c0.2-0.5 0.5-1 1-1.3 0.3-0.2 0.8-0.3 1.4-0.3"/><path d="m83 28.7c0-1.6 0.5-2.9 1.4-3.9s2.2-1.4 3.8-1.4c1.5 0 2.7 0.5 3.6 1.4s1.3 2.2 1.3 3.7c0 1.6-0.5 2.9-1.4 3.8-0.9 1-2.2 1.4-3.8 1.4-1.5 0-2.7-0.5-3.6-1.3-0.9-0.9-1.3-2.2-1.3-3.7m2.4-0.1c0 1 0.2 1.8 0.7 2.4s1.1 0.8 2 0.8c0.8 0 1.5-0.3 1.9-0.8s0.7-1.3 0.7-2.4c0-1.1-0.2-1.9-0.7-2.4s-1.1-0.8-1.9-0.8-1.5 0.3-2 0.8c-0.5 0.6-0.7 1.4-0.7 2.4"/><path d="m96.6 26.2c0 0.3 0.1 0.6 0.3 0.8s0.7 0.4 1.4 0.7c0.9 0.4 1.6 0.8 2 1.2 0.4 0.5 0.6 1 0.6 1.7 0 0.9-0.4 1.7-1.1 2.2-0.7 0.6-1.7 0.8-2.9 0.8-0.4 0-0.9 0-1.4-0.2-0.5-0.1-0.9-0.2-1.3-0.4v-2.3c0.4 0.3 0.9 0.5 1.4 0.7s0.9 0.3 1.3 0.3c0.5 0 0.9-0.1 1.2-0.2 0.2-0.1 0.4-0.4 0.4-0.7s-0.1-0.6-0.4-0.8-0.7-0.5-1.5-0.8c-0.9-0.4-1.5-0.8-1.8-1.2-0.4-0.5-0.5-1-0.5-1.7 0-0.9 0.4-1.6 1.1-2.2s1.6-0.9 2.7-0.9c0.3 0 0.7 0 1.2 0.1 0.4 0.1 0.8 0.2 1.1 0.3v2.4c-0.3-0.2-0.7-0.4-1.1-0.5s-0.8-0.2-1.2-0.2-0.8 0.1-1 0.3c-0.4 0.1-0.5 0.3-0.5 0.6"/><path d="m101.8 28.7c0-1.6 0.5-2.9 1.4-3.9s2.2-1.4 3.8-1.4c1.5 0 2.7 0.5 3.6 1.4s1.3 2.2 1.3 3.7c0 1.6-0.5 2.9-1.4 3.8s-2.2 1.4-3.8 1.4c-1.5 0-2.7-0.5-3.6-1.3-0.9-0.9-1.3-2.2-1.3-3.7m2.4-0.1c0 1 0.2 1.8 0.7 2.4s1.1 0.8 2 0.8c0.8 0 1.5-0.3 1.9-0.8s0.7-1.3 0.7-2.4c0-1.1-0.2-1.9-0.7-2.4s-1.1-0.8-1.9-0.8-1.5 0.3-2 0.8c-0.5 0.6-0.7 1.4-0.7 2.4"/><path d="m119.6 25.5v5.1c0 1 0.2 1.8 0.7 2.3s1.2 0.8 2.2 0.8c0.3 0 0.7 0 1-0.1s0.6-0.1 0.7-0.2v-1.9c-0.1 0.1-0.3 0.2-0.5 0.2-0.2 0.1-0.3 0.1-0.5 0.1-0.5 0-0.8-0.1-1-0.4s-0.3-0.7-0.3-1.3v-4.6h2.3v-1.9h-2.3v-2.9l-2.3 0.7v2.2h-3.5v-1.2c0-0.6 0.1-1 0.4-1.3s0.6-0.5 1.1-0.5c0.2 0 0.5 0 0.7 0.1s0.3 0.1 0.4 0.2v-2 1c-0.2-0.1-0.4-0.1-0.6-0.1h-0.8c-1 0-1.9 0.3-2.6 1s-1 1.5-1 2.5v1.4h-1.6v1.9h1.6v8h2.3v-8l3.6-1.1z"/></g><g><polygon points="61.5 19.7 61.5 33.5 59.1 33.5 59.1 22.7 59.1 22.7 54.8 33.5 53.3 33.5 48.9 22.7 48.9 22.7 48.9 33.5 46.7 33.5 46.7 19.7 50.1 19.7 54 29.9 54.1 29.9 58.2 19.7" fill="#fff"/></g><g><rect y="6.1" x="11.1" height="13" width="13" fill="#F25022"/><rect y="6.1" x="25.5" height="13" width="13" fill="#7FBA00"/><rect y="20.5" x="11.1" height="13" width="13" fill="#00A4EF"/><rect y="20.5" x="25.5" height="13" width="13" fill="#FFB900"/></g><g fill="#fff"><path d="m46.1 13.7v-7.4h2.8c0.5 0 0.9 0 1.1 0.1 0.4 0.1 0.7 0.2 0.9 0.3 0.2 0.2 0.4 0.4 0.6 0.7 0.1 0.3 0.2 0.6 0.2 1 0 0.6-0.2 1.2-0.6 1.6s-1.1 0.7-2.2 0.7h-1.9v3h-0.9zm1-3.9h1.9c0.6 0 1.1-0.1 1.3-0.4 0.3-0.2 0.4-0.6 0.4-1 0-0.3-0.1-0.6-0.2-0.8-0.2-0.2-0.4-0.4-0.6-0.4s-0.5-0.1-0.9-0.1h-1.9v2.7z"/><path d="m52.5 11c0-1 0.3-1.7 0.8-2.2 0.5-0.4 1-0.6 1.7-0.6s1.3 0.2 1.8 0.7 0.7 1.1 0.7 2c0 0.7-0.1 1.2-0.3 1.6s-0.5 0.7-0.9 0.9-0.8 0.3-1.3 0.3c-0.7 0-1.4-0.2-1.8-0.7-0.4-0.4-0.7-1.1-0.7-2zm1 0c0 0.7 0.1 1.2 0.4 1.5s0.7 0.5 1.1 0.5 0.8-0.2 1.1-0.5 0.4-0.9 0.4-1.6-0.2-1.2-0.5-1.5-0.7-0.5-1.1-0.5c-0.5 0-0.8 0.2-1.1 0.5s-0.3 0.9-0.3 1.6z"/><path d="m59.4 13.7h-0.8v-7.4h0.9v2.6c0.4-0.5 0.9-0.7 1.5-0.7 0.3 0 0.6 0.1 0.9 0.2s0.5 0.3 0.7 0.6c0.2 0.2 0.3 0.5 0.4 0.9 0.1 0.3 0.2 0.7 0.2 1.1 0 0.9-0.2 1.6-0.7 2.1s-1 0.8-1.6 0.8-1.1-0.3-1.5-0.8v0.6zm0-2.8c0 0.6 0.1 1.1 0.3 1.4 0.3 0.5 0.7 0.7 1.2 0.7 0.4 0 0.7-0.2 1-0.5s0.4-0.9 0.4-1.5c0-0.7-0.1-1.2-0.4-1.5s-0.6-0.5-1-0.5-0.7 0.2-1 0.5-0.5 0.8-0.5 1.4z"/><path d="m64.3 7.3v-1h0.9v1h-0.9zm0 6.4v-5.4h0.9v5.3h-0.9z"/><path d="m70.3 11.9l0.9 0.1c-0.1 0.5-0.4 1-0.8 1.3s-0.9 0.5-1.5 0.5c-0.8 0-1.4-0.2-1.9-0.7s-0.7-1.2-0.7-2c0-0.9 0.2-1.6 0.7-2.1s1.1-0.7 1.8-0.7 1.3 0.2 1.8 0.7 0.7 1.2 0.7 2.1v0.2h-4c0 0.6 0.2 1 0.5 1.3s0.7 0.5 1.1 0.5c0.3 0 0.6-0.1 0.9-0.3 0.2-0.2 0.4-0.5 0.5-0.9zm-3-1.4h3c0-0.4-0.2-0.8-0.3-1-0.3-0.3-0.7-0.5-1.1-0.5s-0.8 0.1-1 0.4c-0.4 0.2-0.6 0.6-0.6 1.1z"/><path d="m72.4 13.7v-5.4h0.8v0.8c0.2-0.4 0.4-0.6 0.6-0.7s0.4-0.2 0.6-0.2c0.3 0 0.6 0.1 0.9 0.3l-0.3 0.8c-0.2-0.1-0.4-0.2-0.7-0.2-0.2 0-0.4 0.1-0.5 0.2-0.2 0.1-0.3 0.3-0.3 0.5-0.1 0.3-0.2 0.7-0.2 1.1v2.8h-0.9z"/><path d="m75.3 13.7v-0.7l3.4-4h-1-2.2v-0.7h4.4v0.6l-2.9 3.4-0.6 0.6h1.1 2.5v0.8h-4.7z"/><path d="m83.3 13.7v-0.7l3.4-4h-1-2.2v-0.7h4.4v0.6l-2.9 3.4-0.6 0.6h1.1 2.5v0.8h-4.7z"/><path d="m92.8 13.7l-1.6-5.3h0.9l0.9 3.1 0.3 1.1c0-0.1 0.1-0.4 0.3-1.1l0.9-3.1h0.9l0.8 3.1 0.3 1 0.3-1 0.9-3.1h0.9l-1.7 5.3h-0.9l-0.9-3.2-0.2-0.9-1.1 4.1h-1z"/><path d="m99.3 7.3v-1h0.9v1h-0.9zm0 6.4v-5.4h0.9v5.3h-0.9z"/><path d="m103.6 12.9l0.1 0.8c-0.3 0.1-0.5 0.1-0.7 0.1-0.3 0-0.6-0.1-0.8-0.2s-0.3-0.2-0.4-0.4-0.1-0.5-0.1-1.1v-3.1h-0.7v-0.7h0.7v-1.3l0.9-0.5v1.9h0.9v0.6h-0.9v3.1 0.5c0 0.1 0.1 0.1 0.2 0.2 0.1 0 0.2 0.1 0.3 0.1h0.5z"/><path d="m104.4 13.7v-5.4h0.8v0.8c0.2-0.4 0.4-0.6 0.6-0.7s0.4-0.2 0.6-0.2c0.3 0 0.6 0.1 0.9 0.3l-0.3 0.8c-0.2-0.1-0.4-0.2-0.7-0.2-0.2 0-0.4 0.1-0.5 0.2-0.2 0.1-0.3 0.3-0.3 0.5-0.1 0.3-0.2 0.7-0.2 1.1v2.8h-0.9z"/><path d="m107.8 15.7l-0.1-0.9c0.2 0.1 0.4 0.1 0.5 0.1 0.2 0 0.4 0 0.5-0.1s0.2-0.2 0.3-0.3c0.1-0.1 0.1-0.3 0.3-0.7 0-0.1 0-0.1 0.1-0.2l-2-5.4h1l1.1 3.1c0.1 0.4 0.3 0.8 0.4 1.2 0.1-0.4 0.2-0.8 0.4-1.2l1.1-3.1h0.9l-2 5.4c-0.2 0.6-0.4 1-0.5 1.2-0.2 0.3-0.3 0.5-0.6 0.7-0.2 0.1-0.5 0.2-0.7 0.2-0.3 0.1-0.5 0.1-0.7 0z"/><path d="m113 13.7v-5.4h0.8v0.8c0.4-0.6 1-0.9 1.7-0.9 0.3 0 0.6 0.1 0.9 0.2s0.5 0.3 0.6 0.5 0.2 0.4 0.3 0.7c0 0.2 0.1 0.5 0.1 0.9v3.3h-0.9v-3.3c0-0.4 0-0.6-0.1-0.8s-0.2-0.3-0.4-0.4c-0.2-0.3-0.4-0.3-0.6-0.3-0.4 0-0.7 0.1-1 0.4-0.3 0.2-0.4 0.7-0.4 1.4v2.9h-1z"/><path d="m118.7 15.7l-0.1-0.9c0.2 0.1 0.4 0.1 0.5 0.1 0.2 0 0.4 0 0.5-0.1s0.2-0.2 0.3-0.3c0.1-0.1 0.1-0.3 0.3-0.7 0-0.1 0-0.1 0.1-0.2l-2-5.4h1l1.1 3.1c0.1 0.4 0.3 0.8 0.4 1.2 0.1-0.4 0.2-0.8 0.4-1.2l1.1-3.1h0.9l-2 5.4c-0.2 0.6-0.4 1-0.5 1.2-0.2 0.3-0.3 0.5-0.6 0.7s-0.5 0.2-0.7 0.2c-0.3 0.1-0.5 0.1-0.7 0z"/></g></symbol><symbol id="wyborcza-logo-square" viewBox="0 0 28 28"><rect fill="currentColor" width="12.22" height="18.84" x="3.56" y="4.58"/><polygon fill="inherit" points="26.73 10.38 25.81 10.38 24.16 16.62 24.03 16.61 22.14 11.2 23.26 10.54 23.26 10.38 18.55 10.38 18.55 10.54 19.29 11.21 22.25 19.53 23.95 19.53 25.31 14.44 25.45 14.44 27.25 19.53 28 19.53 28 14.03 26.73 10.38"/></symbol><symbol id="wyborcza-logo" viewBox="0 0 32.371646 48.557468"><path d="m 58.099892,473.7838 32.371645,0 0,-48.55747 -32.371645,0 0,48.55747 z"/></symbol></svg>
+
+<!-- svgModule v1.0.4 -->
+ +<!--10185197, [ null ], aggregatorModule--> +
+
+ + + + + + +
+
+
+
+
+
+
+ + + + + + +
+ + + +<script type="text/javascript">
+(function(){"undefined"!==typeof gazeta_pl&&"undefined"!==typeof gazeta_pl.mobileInfo&&gazeta_pl.mobileInfo.hasOwnProperty("isMobileDevice")&&(document.body.className+=!0===gazeta_pl.mobileInfo.isMobileDevice?" mobile_client":" desktop_client")})();
+</script>
+
+<script>
+
+
+
+
+ var userStateCls = 'theme_white';
+
+
+var htmlTag = document.getElementsByTagName('html')[0];
+htmlTag.className = htmlTag.className + ' ' + userStateCls;
+</script>
+
+<div id="theme_holder" class="mcBan"></div>
+
+ + + +<script> + var wyborcza_pl = wyborcza_pl || {}; + + wyborcza_pl.userInfo = { + loggedIn: false, + loggedInSoft: false, + email: "", + channel: "", + nick: "", + nickConfirmationPending: false + }; +</script> + + + + + + + + + +<!-- x16 capParams: pl.com.agora.squid.portal2.dto.CapParameters@35596587[active=false,inProgress=false,validUntil=<null>,validPeriod=<null>,recurring=false,paymentUnfinished=false,unfinishedOrderId=<null>,offerId=<null>,lastPayment=<null>,inProgressOrderId=<null>,consentOrderId=<null>,defaultSalesProductId=true,capVisible=false,email=<null>,emailHash=<null>,marketingMessage=<null>,loginChannel=<null>,loginMode=<null>,nick=<null>,nickConfirmationPending=false,loggedIn=false,loggedInSoft=false] --> + + + + + <nav id="wyborczaHat"> + <div class="container-outer page-cap"> + <div class="container-inner"> + <div class="grid-row"> + <div id="wH_container"> + + <section id="wH_nav"> +<div id="wH_logo"> +<a href="http://www.wyborcza.pl/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka2015', 'czapeczka_logo', 'klik_wyborcza.pl',,false]);"> +<span>Wyborcza.pl</span> +<img alt="Wyborcza.pl" src="https://static.im-g.pl/i/obrazki/wyborcza/wyborcza_pl.svg" data-fallback="https://bis.gazeta.pl/im/2/19701/m19701792.png" width="110" height="20" /> +</a> +</div> +<div id="wH_menu"> +<div id="wH_menu_icon"> +<svg class="badge-symbol"> +<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#burger"></use> +</svg> +</div> +<div id="wH_menu_wrapper"> +<div id="wH_links_nav"> +<header><h5>Wyborcza.pl</h5></header> +<ul> +</ul> +</div> +<div id="wH_links_main"> +<header><h5>Nasze serwisy</h5></header> +<ul> +<li class="wH_more"> +<span>Magazyny</span> +<div class="wH_links_sub"> +<ul> +<li> +<a href="http://wyborcza.pl/duzyformat/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Duy Format',,false]);"> +Duy Format +</a> +</li> +<li> +<a href="http://wyborcza.pl/magazyn/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Magazyn witeczny',,false]);"> +Magazyn witeczny +</a> +</li> +<li> +<a href="http://wyborcza.pl/alehistoria/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Ale Historia',,false]);"> +Ale Historia +</a> +</li> +<li> +<a href="http://wyborcza.pl/TylkoZdrowie/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Tylko Zdrowie',,false]);"> +Tylko Zdrowie +</a> +</li> +<li> +<a href="http://wyborcza.pl/ksiazki/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Ksiki. Magazyn do czytania',,false]);"> +Ksiki. Magazyn do czytania +</a> +</li> +<li> +<a href="http://wyborcza.pl/osiemdziewiec/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Osiem Dziewi',,false]);"> +Osiem Dziewi +</a> +</li> +<li> +<a href="http://wyborcza.pl/Opiekun/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Opiekun',,false]);"> +Opiekun +</a> +</li> +</ul> +</div> +</li> +<li> +<a href="http://wyborcza.biz/biznes/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Wyborcza.biz',,false]);">Wyborcza.biz</a> +</li> +<li> +<a href="http://wyborcza.pl/0,156282.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Wyborcza.Tech',,false]);">Wyborcza.Tech</a> +</li> +<li> +<a href="http://wyborcza.pl/0,160795.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Wyborcza Classic',,false]);">Wyborcza Classic</a> +</li> +<li> +<a href="http://wyborcza.pl/wiecejswiata/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Wicej wiata',,false]);">Wicej wiata</a> +</li> +<li> +<a href="http://wyborcza.pl/0,87647.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Witamy w Polsce',,false]);">Witamy w Polsce</a> +</li> +<li> +<a href="http://biqdata.wyborcza.pl/biqdata/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_BIQdata',,false]);">BIQdata</a> +</li> +<li> +<a href="http://sonar.wyborcza.pl/sonar/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Sonar',,false]);">Sonar</a> +</li> +<li> +<a href="http://www.wysokieobcasy.pl/wysokie-obcasy/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Wysokie Obcasy',,false]);">Wysokie Obcasy</a> +</li> +<li> +<a href="http://cojestgrane24.wyborcza.pl/cjg24/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Co Jest Grane 24',,false]);">Co Jest Grane 24</a> +</li> +<li> +<a href="http://www.logo24.pl/Logo24/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Logo 24',,false]);">Logo 24</a> +</li> +<li> +<a href="http://magazyn-kuchnia.pl/magazyn-kuchnia/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Magazyn Kuchnia',,false]);">Magazyn Kuchnia</a> +</li> +<li> +<a href="http://www.gazeta.pl/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Gazeta.pl',,false]);">Gazeta.pl</a> +</li> +<li class="wH_more"> +<span>Wiadomoci</span> +<div class="wH_links_sub"> +<ul> +<li> +<a href="http://wyborcza.pl/7,101707,24410372,michael-jackson-bral-z-dziecmi-sluby-na-niby-po-premierze.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Michael Jackson: Leaving Neverland',,false]);"> +Michael Jackson: Leaving Neverland +</a> +</li> +<li> +<a href="http://wyborcza.pl/7,75398,24418381,rada-mediow-narodowych-nie-odwola-kurskiego-wniosek-nie-zostal.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Rada Mediw Narodowych: Jarosaw Kurski',,false]);"> +Rada Mediw Narodowych: Jarosaw Kurski +</a> +</li> +<li> +<a href="http://warszawa.wyborcza.pl/warszawa/7,54420,24417372,reprywatyzacja-adwokat-robert-nowaczyk-zeznaje-przed-komisja.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Komisja reprywatyzacja: Robert Nowaczyk',,false]);"> +Komisja reprywatyzacja: Robert Nowaczyk +</a> +</li> +<li> +<a href="http://wyborcza.pl/7,75399,24416982,szesc-ofiar-dziennie-na-morzu-srodziemnym.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Morze rdziemne',,false]);"> +Morze rdziemne +</a> +</li> +<li> +<a href="http://wyborcza.pl/7,75399,24416239,wir-polarny-w-usa-sa-ofiary-smiertelne.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_USA: wir polarny',,false]);"> +USA: wir polarny +</a> +</li> +<li> +<a href="http://warszawa.wyborcza.pl/warszawa/7,54420,24415953,bialoleka-w-klebach-dymu-plonie-hala-ewakuowano-setke-osob.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Biaoka: hala na eraniu',,false]);"> +Biaoka: hala na eraniu +</a> +</li> +<li> +<a href="http://rzeszow.wyborcza.pl/rzeszow/7,34962,24416275,ponad-100-osob-zamknietych-w-sadzie-w-przemyslu-jest-informacja.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Przemyl: wglik',,false]);"> +Przemyl: wglik +</a> +</li> +<li> +<a href="http://wyborcza.pl/7,75398,24415430,tasmy-kaczynskiego-wiadomosci-tvp-o-pazernej-wyborczej.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Wiadomoci TVP: tamy Kaczyskiego',,false]);"> +Wiadomoci TVP: tamy Kaczyskiego +</a> +</li> +<li> +<a href="http://wyborcza.pl/7,75398,24415498,tasmy-kaczynskiego-nowe-nagranie-prezesa-pis-beda-mowic-ze.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Tamy Kaczyskiego',,false]);"> +Tamy Kaczyskiego +</a> +</li> +<li> +<a href="http://wyborcza.pl/7,75398,24415440,w-sejmie-debata-o-nbp-poslanka-po-zablokowala-mownice.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Sejm: NBP',,false]);"> +Sejm: NBP +</a> +</li> +<li> +<a href="http://wyborcza.pl/7,75398,24415454,tasmy-kaczynskiego-sprawdzamy-czy-prezes-pis-zlamal-prawo.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Jarosaw Kaczyski: tamy Kaczyskiego',,false]);"> +Jarosaw Kaczyski: tamy Kaczyskiego +</a> +</li> +<li> +<a href="http://wyborcza.pl/7,75398,24415443,tasmy-kaczynskiego-partia-buduje-wiezowiec-drugie-nagranie.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Tamy Kaczyskiego - drugie nagranie audio i stenogram',,false]);"> +Tamy Kaczyskiego - drugie nagranie audio i stenogram +</a> +</li> +<li> +<a href="http://wyborcza.pl/7,155287,24414148,europejski-raport-o-smogu-polska-walczy-za-slabo.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Smog',,false]);"> +Smog +</a> +</li> +<li> +<a href="http://wyborcza.pl/7,155287,24414565,zlote-dziecko-pis-na-zakrecie-kariery.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Micha Krupiski: tamy Kaczyskiego',,false]);"> +Micha Krupiski: tamy Kaczyskiego +</a> +</li> +<li> +<a href="http://krakow.wyborcza.pl/krakow/7,44425,24415702,wysadzony-bankomat-przy-ul-teligi-powazne-utrudnienia.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Bankomat: ul. Tegli w Krakowie',,false]);"> +Bankomat: ul. Tegli w Krakowie +</a> +</li> +<li> +<a href="http://wyborcza.pl/7,75398,24415472,tasmy-kaczynskiego-pekao-sa-nie-zaprzecza-ze-prezes-krupinski.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Tamy Kaczyskiego: Pekao SA, Micha Krupiski',,false]);"> +Tamy Kaczyskiego: Pekao SA, Micha Krupiski +</a> +</li> +<li> +<a href="http://krakow.wyborcza.pl/krakow/7,44425,24412575,smiertelny-wypadek-w-babicach-kierowca-zginal-na-obwodnicy.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Wypadek w Babicach',,false]);"> +Wypadek w Babicach +</a> +</li> +<li> +<a href="http://poznan.wyborcza.pl/poznan/7,36001,24413925,prezydent-poznania-jacek-jaskowiak-w-szpitalu-na-prawicowych.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Jacek Jakowiak',,false]);"> +Jacek Jakowiak +</a> +</li> +<li> +<a href="http://krakow.wyborcza.pl/krakow/7,44425,24412932,tatry-pod-rysami-zeszla-lawina.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Tatry: lawina pod Rysami',,false]);"> +Tatry: lawina pod Rysami +</a> +</li> +<li> +<a href="http://warszawa.wyborcza.pl/warszawa/7,54420,24411986,pierwszy-pociag-metra-dojechal-na-targowek-kiedy-otwarcie.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Metro Targwek',,false]);"> +Metro Targwek +</a> +</li> +<li> +<a href="http://wyborcza.pl/7,75398,24408844,tasmy-kaczynskiego-partia-buduje-wiezowiec-nagranie-audio.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Tamy Kaczyskiego: audio i stenogram',,false]);"> +Tamy Kaczyskiego: audio i stenogram +</a> +</li> +<li> +<a href="http://wyborcza.pl/alehistoria/7,162090,24414810,31-stycznia-niemcy-do-amerykanow-dajcie-nam-trzy-miesiace.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_31 stycznia',,false]);"> +31 stycznia +</a> +</li> +<li> +<a href="http://wyborcza.pl/TylkoZdrowie/7,157387,24415511,pogoda-w-czwartek-31-stycznia-2019-r.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Pogoda w czwartek 31 stycznia 2019 r.',,false]);"> +Pogoda w czwartek 31 stycznia 2019 r. +</a> +</li> +</ul> +</div> +</li> +<li class="wH_more"> +<span>Serwisy lokalne</span> +<div class="wH_links_sub"> +<ul> +<li> +<a href="http://bialystok.wyborcza.pl/bialystok/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Biaystok',,false]);"> +Biaystok +</a> +</li> +<li> +<a href="http://bielskobiala.wyborcza.pl/bielskobiala/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Bielsko-Biaa',,false]);"> +Bielsko-Biaa +</a> +</li> +<li> +<a href="http://bydgoszcz.wyborcza.pl/bydgoszcz/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Bydgoszcz',,false]);"> +Bydgoszcz +</a> +</li> +<li> +<a href="http://czestochowa.wyborcza.pl/czestochowa/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Czstochowa',,false]);"> +Czstochowa +</a> +</li> +<li> +<a href="http://gliwice.wyborcza.pl/gliwice/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Gliwice',,false]);"> +Gliwice +</a> +</li> +<li> +<a href="http://gorzow.wyborcza.pl/gorzow/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Gorzw Wielkopolski',,false]);"> +Gorzw Wielkopolski +</a> +</li> +<li> +<a href="http://katowice.wyborcza.pl/katowice/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Katowice',,false]);"> +Katowice +</a> +</li> +<li> +<a href="http://kielce.wyborcza.pl/kielce/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Kielce',,false]);"> +Kielce +</a> +</li> +<li> +<a href="http://krakow.wyborcza.pl/krakow/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Krakw',,false]);"> +Krakw +</a> +</li> +<li> +<a href="http://lublin.wyborcza.pl/lublin/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Lublin',,false]);"> +Lublin +</a> +</li> +<li> +<a href="http://lodz.wyborcza.pl/lodz/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_d',,false]);"> +d +</a> +</li> +<li> +<a href="http://olsztyn.wyborcza.pl/olsztyn/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Olsztyn',,false]);"> +Olsztyn +</a> +</li> +<li> +<a href="http://opole.wyborcza.pl/opole/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Opole',,false]);"> +Opole +</a> +</li> +<li> +<a href="http://plock.wyborcza.pl/plock/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Pock',,false]);"> +Pock +</a> +</li> +<li> +<a href="http://poznan.wyborcza.pl/poznan/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Pozna',,false]);"> +Pozna +</a> +</li> +<li> +<a href="http://radom.wyborcza.pl/radom/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Radom',,false]);"> +Radom +</a> +</li> +<li> +<a href="http://rzeszow.wyborcza.pl/rzeszow/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Rzeszw',,false]);"> +Rzeszw +</a> +</li> +<li> +<a href="http://sosnowiec.wyborcza.pl/sosnowiec/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Sosnowiec',,false]);"> +Sosnowiec +</a> +</li> +<li> +<a href="http://szczecin.wyborcza.pl/szczecin/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Szczecin',,false]);"> +Szczecin +</a> +</li> +<li> +<a href="http://torun.wyborcza.pl/torun/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Toru',,false]);"> +Toru +</a> +</li> +<li> +<a href="http://trojmiasto.wyborcza.pl/trojmiasto/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Trjmiasto',,false]);"> +Trjmiasto +</a> +</li> +<li> +<a href="http://warszawa.wyborcza.pl/warszawa/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Warszawa',,false]);"> +Warszawa +</a> +</li> +<li> +<a href="http://wroclaw.wyborcza.pl/wroclaw/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Wrocaw',,false]);"> +Wrocaw +</a> +</li> +<li> +<a href="http://zielonagora.wyborcza.pl/zielonagora/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Zielona Gra',,false]);"> +Zielona Gra +</a> +</li> +</ul> +</div> +</li> +<li class="wH_more"> +<span>Inne serwisy</span> +<div class="wH_links_sub"> +<ul> +<li> +<a href="http://www.tokfm.pl/Tokfm/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Tok FM',,false]);"> +Tok FM +</a> +</li> +<li> +<a href="http://www.edziecko.pl/edziecko/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Dziecko',,false]);"> +Dziecko +</a> +</li> +<li> +<a href="http://zdrowie.gazeta.pl/Zdrowie/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Zdrowie',,false]);"> +Zdrowie +</a> +</li> +<li> +<a href="http://www.blox.pl/html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Blogi',,false]);"> +Blogi +</a> +</li> +<li> +<a href="http://pogoda.gazeta.pl/" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Pogoda',,false]);"> +Pogoda +</a> +</li> +<li> +<a href="http://www.sport.pl/sport/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Sport',,false]);"> +Sport +</a> +</li> +<li> +<a href="http://podroze.gazeta.pl/podroze/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Podre',,false]);"> +Podre +</a> +</li> +<li> +<a href="http://forum.gazeta.pl/forum/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Forum',,false]);"> +Forum +</a> +</li> +<li> +<a href="http://forum.gazeta.pl/forum/0,0.html" onClick="_gaq.push(['_trackEvent', 'czapeczka_nowa', 'czapeczka_menu', 'klik_Forum',,false]);"> +Forum +</a> +</li> +</ul> +</div> +</li></ul> +</div> +</div> +</div> +</section><!-- UZREditor --><!--22573586,aliasOf--><!-- htmEOF -->
+ + +<div id="wH_messages"> + + + + + + + + + <div id="wH_user_msg" class="mcBan"> + <div id="wH_user_msg_content" data-cta="1sqbl-5basic" data-cta-category="czapeczka napis"> + <a href="http://prenumerata.wyborcza.pl/lp/0,145442.html?cta=1sqbl-2an-5basic">Wyprbuj od 19,90 z</a> + </div> + </div> + <svg class="badge-symbol"> + <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#bell"></use> + </svg> + <div id="wH_action" class="mcBan"> + <a id="wH_action_btn" class="wH_btn" data-cta="1sqbl-2an-5basic" data-cta-category="czapeczka przycisk" href="http://prenumerata.wyborcza.pl/lp/0,145442.html?cta=1sqbl-2an-5basic">Kup teraz</a> + </div> + +<!-- <div id="wH_user_msg" class="mcBan"> + <div id="wH_user_msg_content" data-cta="1sqbl-5basic" data-cta-category="czapeczka napis"> + <a href="http://prenumerata.wyborcza.pl/lp/0,166591,24048616.html?cta=1sqbl-3an-550journalists?actionId=7518533d-2606-4905-bdaf-00a9eb088c7b&campaignId=4e827602-54e2-404d-8b18-d9e98d19ee3c">Twoje sprawy, nasza praca. Subskrybuj za p ceny i czytaj Wyborcza.pl</a> + </div> + </div> + <svg class="badge-symbol"> + <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#bell"></use> + </svg> + <div id="wH_action" class="mcBan"> + <a id="wH_action_btn" class="wH_btn" data-cta="1sqbl-2an-5basic" data-cta-category="czapeczka przycisk" href="http://prenumerata.wyborcza.pl/lp/0,166591,24048616.html?cta=1sqbb-3an-550journalists?actionId=e246120c-339b-4506-ab5c-1a28c277b636&campaignId=4e827602-54e2-404d-8b18-d9e98d19ee3c">Skorzystaj z promocji -50%</a> + </div> --> + + + + +</div> + + +<div id="wH_activity"> + + + + + + <div id="wH_user"> + <form id="wH_login_form" method="POST"> + <input type="hidden" name="SsoSessionPermanent"> + <input type="hidden" name="host"> + <div id="wH_login_btn" class="hat-log-in" onclick="_gaq.push(['_trackEvent', 'CzapeczkaRWD', 'Logowanie', location.hostname, , true]);"> + Logowanie + <svg class="badge-symbol"> + <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#user"></use> + </svg> + </div> + </form> + </div> + + + + +</div> + + + </div> + </div> + </div> + </div> + </nav> + + + + + + + + + + +<!-- squidCapBean v1.1 --> + +<!--9638935, [ /fix/modules/wyborcza/cap/cap.jsp ], squidCapBean--> +
+
+<script type="text/javascript">
+ //<![CDATA[
+ try {
+ var domain_marketing_params = {
+ counter: null,
+ disable: false,
+ differentiateDomainMarketing: 'wsj2017biz',
+ source: '',
+ tags: ["jako powietrza","smog","wgiel","zanieczyszczenie powietrza"],
+ clsarticle: '',
+ marketingDomainRequestParams: '',
+ isActive: 'false',
+ validPeriod: '',
+ subnetCompany: '',
+ freeActivation: 'unknown',
+ agreementState: 'false',
+ loginChannel: '',
+ dmcampaign: '',
+ hardPaywall: true,
+ recoveryMethod: '',
+ rodo22: 'false'
+ };
+ } catch (e) { };
+ //]]>
+</script>
+
+<script>
+ customDataLayer = window.customDataLayer || {};
+ customDataLayer.customer = customDataLayer.customer || {};
+ customDataLayer.customer.state = 'anonymous';
+ customDataLayer.customer.validPeriod = '';
+ document.cookie = 'customDataLayer_customer=' + encodeURIComponent(JSON.stringify(customDataLayer.customer)) + '; domain=wyborcza.pl; path=/';
+</script>
+
+<!-- domain-marketing-module v1.0 -->
+ +<!--13289421, [ /fix/modules/wyborcza/portal/domainMarketingModule.jsp ], domainMarketingModule--> +
+
+
+
+ <div class="container-outer page-top">
+ <div class="container-inner">
+ <div class="grid-row">
+ <div class="grid-col-4-4 column-58">
+
+
+
+ <div class="winieta-promo">
+ <header id="pageHead" data-like-shift="1"> +<div class="c0"> +<div class="imgw"> +<a id="LinkArea:winietabiz" href="http://wyborcza.biz/biznes/0,0.html" title="Wyborcza.biz"><img src="http://static.im-g.pl/i/obrazki/wyborcza2017/winiety_themes/wyborcza_biz.svg" data-fallback="https://bis.gazeta.pl/im/7/22716/m22716587.png" alt="Wyborcza.biz"></a> +</div> +<div class="header-date" id="header-day"><span class="day" id="header-date-day"></span><span class="date" id="header-date-date"></span></div> +<script> +(function() { +var a=document.getElementById("header-date-day"),b=document.getElementById("header-date-date"),c=(now && now.getUTCDate()>1)?new Date(now):new Date,d="niedziela poniedzia\u0142ek wtorek \u015broda czwartek pi\u0105tek sobota".split(" ")[c.getDay()],e=c.getDate(),f=c.getMonth()+1,g=c.getFullYear(),h="";10>e&&(e="0"+e.toString());10>f&&(f="0"+f.toString());h=e+"."+f+"."+g;a.innerHTML=d;b.innerHTML=h; +})(); +</script> +</div> +</header><!-- UZREditor --><!-- htmEOF --> +<!--22776313, [ /htm/22776/j22776313.htm ], null--> +
+
+
+
+<div id="071-WINIETA" class="adviewDFPBanner DFP-071-WINIETA">
+ <span class="banLabel" style="display: none;">REKLAMA</span>
+ <div id='div-gpt-ad-071-WINIETA-0'>
+ <script type='text/javascript'>
+ if(dfpParams.slots['071-WINIETA'] && dfpParams.slots['071-WINIETA'].autoLoad) {
+ if (adviewDFP && adviewDFP.scrollSlot) adviewDFP.scrollSlot.push('071-WINIETA');
+ } else {
+ googletag.cmd.push(function() { googletag.display('div-gpt-ad-071-WINIETA-0'); });
+ }
+
+ </script>
+ </div>
+</div>
+
+
+<!-- v2.2 --> +<!--11070029, [ /tpl/ads/prod/dfpSlot.jsp ], dfpBanersSlotBean--> +
+ </div>
+
+
+
+
+
+
+
+
+<!-- Menubar20_nst API ver 1.0 -->
+
+ <!-- ver 0.45 newMenuBar:false, menuType: 0, nst:, szukaj:false, szukajNSTAlias:null -->
+
+ <nav data-position="nawigacja" id="navH" class="level0"><ul class='p0'><li id='e1' class=' '><a href='http://wyborcza.pl/0,155287.html#TRNavSST' title='Aktualnoci '>Aktualnoci</a></li><li id='e2' class=' '><a href='http://wyborcza.biz/biznes/0,147768.html#TRNavSST' title='Podatki '>Podatki</a></li><li id='e3' class=' '><a href='http://wyborcza.biz/biznes/0,147582.html#TRNavSST' title='Finanse osobiste '>Finanse osobiste</a></li><li id='e4' class=' '><a href='http://wyborcza.biz/biznes/0,147880.html#TRNavSST' title='ZUS i emerytury '>ZUS i emerytury</a></li><li id='e5' class=' '><a href='http://wyborcza.biz/Gieldy/0,114514.html#TRNavSST' title='Gieda '>Gieda</a></li><li id='e6' class=' '><a href='http://wyborcza.biz/Waluty/0,111138,8932151,,,Kursy_srednie_walut_NBP,A.html#TRNavSST' title='Kursy walut '>Kursy walut</a></li><li id='e7' class=' active'><a href='http://wyborcza.biz/biznes/0,147743.html#TRNavSST' title='Konsument i zakupy '>Konsument i zakupy</a></li><li id='e8' class=' '><a href='http://wyborcza.biz/pieniadzeekstra/0,0.html#TRNavSST' title='Pienidze Ekstra '>Pienidze Ekstra</a></li><li id='e9' class=' '><a href='http://wyborcza.biz/biznes/0,147758.html#TRNavSST' title='Nieruchomoci '>Nieruchomoci</a></li><li id='e10' class=' '><a href='http://wyborcza.biz/biznes/0,159911.html#TRNavSST' title='Praca '>Praca</a></li><li id='e11' class=' '><a href='http://www.komunikaty.pl/komunikaty/#TRNavSST' title='Komunikaty '>Komunikaty</a></li><li id='e12' class=' '><a href='##TRNavSST' title='Wicej '>Wicej</a><ul class='p1'><li id='e12_1' class=' '><a href='http://wyborcza.pl/0,155290.html#TRNavSST' title='Opinie '>Opinie</a></li><li id='e12_2' class=' '><a href='http://wyborcza.biz/biznes/0,163992.html#TRNavSST' title='Finanse maej firmy '>Finanse maej firmy</a></li><li id='e12_3' class=' '><a href='http://wyborcza.biz/biznes/0,147584.html#TRNavSST' title='Wasna firma '>Wasna firma</a></li><li id='e12_4' class=' '><a href='http://wyborcza.pl/0,156282.html#TRNavSST' title='Technologie '>Technologie</a></li><li id='e12_5' class=' '><a href='http://wyborcza.biz/biznes/0,156481.html#TRNavSST' title='Motoryzacja i podre '>Motoryzacja i podre</a></li><li id='e12_6' class=' '><a href='http://wyborcza.biz/akcjespecjalne/0,154807.html#TRNavSST' title='Przedsibiorca Roku '>Przedsibiorca Roku</a></li></ul></li></ul></nav>
+
+
+ <div class="mod mod_search" id="pageSearch"><div class="visible"><form><fieldset><input id="pageSearchQ"><input value="Szukaj" type="submit"></fieldset></form></div><div class="hidden"><form data-target="serwis" data-default method="get" action="http://wyborcza.biz/biznes/wyszukaj/artykul"><p><input type="hidden" data-query name="query"/><input value="" name="" type="hidden"></p></form></div></div> +<!--21407274, [ /htm/21407/n21407274.htm ], null--> +
+ </div>
+ </div>
+ </div>
+ </div>
+
+
+
+ <section class="ads ads-top container-outer">
+ <div class="container-inner">
+ <div class="grid-row">
+ <div class="grid-col-wide">
+
+
+
+<div id="001-TOPBOARD" class="adviewDFPBanner DFP-001-TOPBOARD">
+ <span class="banLabel" style="display: none;">REKLAMA</span>
+ <div id='div-gpt-ad-001-TOPBOARD-0'>
+ <script type='text/javascript'>
+ if(dfpParams.slots['001-TOPBOARD'] && dfpParams.slots['001-TOPBOARD'].autoLoad) {
+ if (adviewDFP && adviewDFP.scrollSlot) adviewDFP.scrollSlot.push('001-TOPBOARD');
+ } else {
+ googletag.cmd.push(function() { googletag.display('div-gpt-ad-001-TOPBOARD-0'); });
+ }
+
+ </script>
+ </div>
+</div>
+
+
+<!-- v2.2 --> +<!--10185259, [ /tpl/ads/prod/dfpSlot.jsp ], dfpBanersSlotBean--> +
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <main class="content container-inner">
+
+
+
+ <div class="grid-row">
+ <div class="grid-col-wide">
+
+
+
+
+
+
+
+
+
+
+ <header id="art-header" class="art-header ">
+ <div id="art-tags" class="art-tags">
+
+
+
+
+
+ <a href="http://wyborcza.biz/biznes/0,147743.html" title="Konsument i zakupy"
+ class="art-headline-tag"> <span class="art-tag-label">Konsument i zakupy</span></a>
+
+
+
+
+
+ </div>
+
+ <div class="art-headline" role="headline">
+
+
+
+
+ <h1 class="art-title">Pomys na biznes: chusta, ktra chroni przed smogiem</h1>
+ </div>
+
+ <div class="art-header-meta">
+
+
+ <div class="art-authors">
+ <span class="art-author">Micha Frk</span>
+ </div>
+
+
+
+ <div class="art-author-meta-container">
+
+ <time id="art-datetime" class="art-datetime"
+ datetime="2019-01-31">31 stycznia 2019 | 15:32</time>
+
+ </div>
+ </div>
+ </header>
+
+
+
+<!-- articleMetadata v1.4 -->
+ +<!--9534510, [ /fix/modules/wyborcza/articleMetadata.jsp ], articleMetadata--> +
+ </div>
+ </div>
+
+
+
+
+ <div class="grid-row splint-parent">
+
+
+
+ <section class="article-and-social grid-col-3-4" >
+ <div class="article">
+ <article class="article-content">
+
+
+
+ <div class="article-image">
+
+
+
+
+
+
+
+
+<!-- 0/4,24414148 --> +<!-- 1/4,24376685 --> +<!-- 2/4,24349296 --> +<!-- 3/4,24302580 --> +<!-- 0/4,24414148 --> +<!-- 1/4,24376685 --> +<!-- 2/4,24349296 --> +<!-- 3/4,24302580 --> +
+
+
+
+
+
+
+ <div id="gazeta_article_image"><!-- rel max = 4 -->
+ <div class="article-image-photo">
+
+ <img border="0" src="https://bi.im-g.pl/im/f7/49/17/z24418295Q,Prace-nad-projektem-chusty-antysmogowej-rozpoczely.jpg" title="Prace nad projektem chusty antysmogowej rozpoczy si w lutym 2018 roku, a w styczniu 2019 pojawia si na rynku" alt="Prace nad projektem chusty antysmogowej rozpoczy si w lutym 2018 roku, a w styczniu 2019 pojawia si na rynku">
+ <figure class="box-label-related"><svg class="icon"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#gallery"></use></svg><figcaption>1 ZDJĘCIE</figcaption></figure>
+
+
+
+
+
+
+
+ </div>
+ <div class="article-image-desc">
+ <p class="desc">Prace nad projektem chusty antysmogowej rozpoczy si w lutym 2018 roku, a w styczniu 2019 pojawia si na rynku<span> (MATERIAY PRASOWE)</span></p>
+
+
+
+ </div>
+
+ </div><!-- #gazeta_article_image -->
+
+
+ <!-- bl sstx -->
+
+
+
+
+
+
+
+
+
+
+
+
+
+<!-- plistaRelated v 1.04 -->
+ +<!--9638972, [ /fix/modules/plistaRelated.jsp ], null--> +
+ </div>
+
+
+
+ + + + +<section class="article-lead">Filtr ma tak dokadny, e zatrzymuje nawet wirusy i bakterie. Trjka wrocawian stworzya chust, ktra chroni przed smogiem. </section> + + + + + + + + <section class="article-text"> + +
+
+
+<!-- SQUID_PARAMS: SquidPaywallLock [subnetAccessGranted=false, cookieDisabled=false, javaScriptDisabled=false, referrer=null, saleInProgress=false, paymentUnfinished=false, consentOrderId=null, unfinishedOrderId=null, validUntil=null, loggedInHard=false, loggedInSoft=false, counterShowIndicator=null, remainingVisitTime=null, exclusiveContent=true, loopholeDetected=false, isAssetUnlocked=false, closeByUrlParameter=null, aliasLock=/aliasy/dm/planbhardpaywallclosearticleall.htm, aliasUpper=, aliasLower=, aliasLink=, placeholders={standardLock=/aliasy/dm/planbclosearticleanonymous.htm, specialSectionLowerOpen=/aliasy/paywall/v12/empty.htm, standardLockUserLoggedIn=/aliasy/dm/planbclosearticleinactive.htm, activeOpenUpper=/aliasy/paywall/v12/empty.htm, upperOpen=/aliasy/dm/planbopenarticleupperzajawka.htm, subnetOpenLower=/aliasy/paywall/v12/empty.htm, closeByUrl=/aliasy/wejsciezmailingu.htm, loopholeDetected=/aliasy/dm/planbclosearticleloophole.htm, googleReferrerAnonymous=/aliasy/dm/planbopenarticlegoogleanonymous.htm, lowerOpenExclusive=/aliasy/paywall/v12/empty.htm, gazetaReferrerWeekendInactive=/aliasy/dm/planbclosearticlegaplweekend.htm, facebookReferrerAnonymous=/aliasy/dm/planbopenarticlefacebookanonymous.htm, cookieDisabled=/aliasy/wyborcza/nocookiesbox.htm, standardOpenUserLoggedIn=/aliasy/dm/pustazajawkav2.htm, gazetaReferrerNormalInactive=/aliasy/dm/planbopenarticlegaplpnpt.htm, specialSectionUpperOpen=/aliasy/paywall/v12/empty.htm, gazetaReferrerNormal=/aliasy/dm/planbopenarticlegaplpnpt.htm, subnetOpenApper=/aliasy/paywall/v12/empty.htm, activeOpenLower=/aliasy/paywall/v12/empty.htm, lowerOpen=/aliasy/dm/planbopenarticleanonymous.htm, exclusiveContentLock=/aliasy/dm/planbhardpaywallclosearticleall.htm, paymentFailed=/aliasy/paywall/v12/nieudana.jsp, serviceNotStarted=/aliasy/paywall/v12/uruchom.jsp, inProgressLock=/aliasy/paywall/v12/czekamy.htm, gazetaReferrerWeekend=/aliasy/dm/planbclosearticlegaplweekend.htm, specialSectionClose=/aliasy/dm/closearticleanonymousroot.htm, facebookReferrerInactive=/aliasy/dm/planbopenarticlefacebookinactive.htm, javascriptDisabled=/aliasy/paywall/v12/nojavascript.htm, upperOpenExclusive=/aliasy/dm/openarticleupperhardpaywall.htm, googleReferrerInactive=/aliasy/dm/planbopenarticlegoogleinactive.htm}, aliasEnabled=true, isPaywallException=false, hashedLogin=null, defaultSalesProductId=true] ; accessEnabled=false ; accessPaid=false ; locked=true -->
+
+<script type="text/javascript">
+ //<![CDATA[
+ try {
+
+ var pw_application_data = {
+ UID: '',
+ RemainingAccess: '',
+ Alias: '/aliasy/dm/planbhardpaywallclosearticleall.htm',
+ Paid: 'false',
+ PaywallType: 'Hard',
+ Access: 'false',
+ AccessTo: ''
+ };
+
+ var pw_article = {
+ url: 'wyborcza.biz/biznes/7,147743,24417936,pomysl-na-biznes-chusta-ktora-chroni-przed-smogiem.html',
+ title: '',
+ lead: 'Filtr ma tak dokadny, e zatrzymuje nawet wirusy i bakterie. Trjka wrocawian stworzya chust, ktra chroni przed smogiem. '
+ };
+
+ var wyborcza_pl = wyborcza_pl || {};
+ wyborcza_pl.bigData = {
+ EVENT_NAME: "pwdataready",
+ init: function () {
+ var a = "undefined" != typeof pw_application_data;
+ a && this.createEvent()
+ },
+ createEvent: function () {
+ var a = wyborcza_pl.bigData,
+ b = {};
+ document.createEvent ? (b = document.createEvent("HTMLEvents"), b.initEvent(a.EVENT_NAME, !0, !0)) : (b = document.createEventObject(), b.eventType = a.EVENT_NAME), b.eventName = a.EVENT_NAME, document.createEvent ? document.dispatchEvent(b) : document.fireEvent("on" + b.eventType, b)
+ }
+ }, wyborcza_pl.bigData.init();
+
+ } catch (e) {
+ }
+ ;
+ //]]>
+</script>
+
+
+
+<script>
+ //<![CDATA[
+ var gazeta_pl = gazeta_pl || {};
+ gazeta_pl.Config = gazeta_pl.Config || {};
+ gazeta_pl.Config.Squid = {
+ domain: 'GQOnEDUr8VUlxDfzolv2hQ',
+ luid: '',
+ dc: ''
+ };
+ //]]>
+</script>
+
+<!-- paywallScriptModule 1.1 -->
+ +<!--13124793, [ /fix/modules/wyborcza/paywall/paywallScriptModule.jsp ], paywallScriptModule--> + +
+
+
+
+
+<!-- paywallUpperPlaceholder 1.1 -->
+ +<!--13124791, [ /fix/modules/wyborcza/paywall/paywallUpperPlaceholder.jsp ], paywallUpperPlaceholder--> + + + + +<html> + <head></head> + <body> + <section class="art_content" itemprop="articleSection"> + <p class="art_paragraph">Choć Wrocław to piękne miasto, często pojawia się w polskiej czołówce tych najbardziej ogarniętych smogiem. Zdarza się, że i w światowym rankingu zajmuje wysokie lokaty. Wszystko zależy od tego, jaka akurat jest pogoda.</p> + <p class="art_paragraph">Trójka zaniepokojonych wrocławian od dawna obserwuje wskazania dwóch oficjalnych stacji pomiarowych. I często aż strach jest wyjść z domu. Zresztą w nim też nie jest dużo lepiej.</p> + <p class="art_paragraph">– Szkodliwe cząsteczki unoszące się w smogu są bardzo małe. Gdy ich stężenie jest wysokie, dostają się również do wewnątrz mieszkań. A to oznacza, że przed smogiem nie da się całkowicie ochronić, zamykając drzwi i okna – opowiada Adam Muszyński oraz Diana i Przemek Jaworscy, założyciele firmy produkującej innowacyjny element ubioru chroniący przed smogiem.</p> + <p class="art_paragraph">Oni, jak i wielu ich znajomych, chcieli zadbać o zdrowie. Jednym ze sposobów na to jest noszenie specjalnej maseczki z filtrem. Do obrazka, na którym ludzie mają pozasłaniane białymi maseczkami twarze, przywykliśmy oglądając relacje z największych miast Azji, gdzie problem smogu jest szczególnie widoczny.</p> + </section> + </body> +</html> + + +<!--13006101, [ /tpl/prod/content/article/modules/article_body_sst.jsp ], awdArticleBodySstModuleTomcat--> + + + + + + + +<!-- percentageOfArticleBehindPaywallModule 1.1 --> + +<!--13270618, [ /fix/modules/wyborcza/percentageOfArticleBehindPaywallModule.jsp ], percentageOfArticleBehindPaywallModule--> + +
+
+
+
+ <div class="mcBan" id="mcBan_1">
+ <section class="padlock +padlock-article-title-align-center +locked +padlock-text-align-center" +data-cta="1pbox-2hd-5basic" data-cta-category="zajawka zamkniecia"> +<header class="padlock-header"> +<figure class="padlock-header-symbol"> +<svg class="icon"> +<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#diamond"></use> +</svg> +</figure> +<span class="padlock-header-msg">Artyku dostpny tylko w prenumeracie cyfrowej Wyborczej</span> +</header> +<div class="padlock-main"> +<div class="padlock-body-container"> +<article class="padlock-article"> +<h3 class="padlock-article-title">Wyprbuj cyfrow Wyborcz</h3> +<p class="padlock-article-lead">Nieograniczony dostp do serwisw informacyjnych, biznesowych, lokalnych i wszystkich magazynw Wyborczej</p> +</article> +<div class="padlock-action" data-utilize-child="true" data-utilize="x"> +<a class="padlock-action-url" href="http://prenumerata.wyborcza.pl/lp/0,145442.html?cta=1pbox-2hd-5basic"> +Wyprbuj teraz od 19,90 z +</a> +</div> +<footer class="padlock-footer"> +<ul class="padlock-footer-list"> +<li class="padlock-footer-list-login"> +<a class="pianoLoginBoxBtn" href="#"> +Zaloguj si +</a> +</li> +<li class="padlock-footer-list-contact"> +<a href="mailto:pomoc@wyborcza.pl"> +Kontakt +</a> +</li> +</ul> +</footer> +</div> +</div> +</section><!-- UZREditor --><!--22768337,aliasOf--><!-- htmEOF -->
+ </div>
+
+
+<!-- paywallBottomPlaceholder 1.1 -->
+ +<!--13124792, [ /fix/modules/wyborcza/paywall/paywallBottomPlaceholder.jsp ], paywallBottomPlaceholder--> + + + </section> + + + + +<!-- articleModule v1.5 --> + +<!--9638918, [ /fix/modules/wyborcza/articleModule.jsp ], wyborczaArticleModule--> +
+
+
+
+
+
+<!-- partnerInfoModule -->
+ +<!--11917370, [ /fix/modules/wyborcza/portal/partnerInfoModule.jsp ], emptyBean--> +
+
+
+
+
+
+
+<!-- ContentHolderExtractorModule v1.0.5 -->
+ +<!--13198008, [ /fix/modules/wyborcza/portal/contentHolderExtractorModule.jsp ], contentHolderExtractorModule--> +
+
+
+
+ </article>
+ <div class="show-survey-quiz">
+
+ </div>
+ </div>
+
+
+
+
+
+
+ <!--supertag-start -->
+
+
+
+
+
+
+
+
+
+
+
+
+ <!-- supertag-end -->
+
+ <section class="post-article">
+
+
+
+ <div class="post-article-content-holder column-54" data-position="54,1-5">
+
+
+ + + + +<!-- (~MODSTART : 7 :21407481) DFP 1.2 --> + + + + +<div class="index zi_index_read_other"> + + <div class="head"> + + <h3> + Inne + </h3> + + + </div> + <div class="body"> + <ul> + + + + + + + + <li class="entry even article" > + + + + + + + + + <div class="imgw"> + + + + <ul> + <li > + <a href="http://wyborcza.biz/biznes/7,147743,24418098,inwestujemy-w-oczyszczacz-powietrza-podpowiadamy-jak-dobrac.html"> + <img src="https://bi.im-g.pl/im/f9/49/17/z24418297II,Oczyszczacze-powietrza-pozwola-nam-zdrowo-oddychac.jpg"/> + </a> + + + </li> + </ul> + + + </div> + + + + + + + + + + + + + + + + + + + + + + + + + + + <span class="base"><a href="http://wyborcza.biz/biznes/0,147743.html">Konsument i zakupy</a> + </span> + + + + + + + + + <h4> + <a title="Inwestujemy w oczyszczacz powietrza. Podpowiadamy, jak dobra urzdzenie i ile to kosztuje" href="http://wyborcza.biz/biznes/7,147743,24418098,inwestujemy-w-oczyszczacz-powietrza-podpowiadamy-jak-dobrac.html">Inwestujemy w oczyszczacz powietrza. Podpowiadamy, jak dobra urzdzenie i ile to kosztuje</a> + </h4> + + + + + + + + + + + + + + + + </li> + + + + + + + + + + + + <li class="entry odd article" > + + + + + + + + + <div class="imgw"> + + + + <ul> + <li > + <a href="http://wyborcza.biz/biznes/7,147743,24417936,pomysl-na-biznes-chusta-ktora-chroni-przed-smogiem.html"> + <img src="https://bi.im-g.pl/im/f7/49/17/z24418295II,Prace-nad-projektem-chusty-antysmogowej-rozpoczely.jpg"/> + </a> + + + </li> + </ul> + + + </div> + + + + + + + + + + + + + + + + + + + + + + + + + + + <span class="base"><a href="http://wyborcza.biz/biznes/0,147743.html">Konsument i zakupy</a> + </span> + + + + + + + + + <h4> + <a title="Pomys na biznes: chusta, ktra chroni przed smogiem" href="http://wyborcza.biz/biznes/7,147743,24417936,pomysl-na-biznes-chusta-ktora-chroni-przed-smogiem.html">Pomys na biznes: chusta, ktra chroni przed smogiem</a> + </h4> + + + + + + + + + + + + + + + + </li> + + + + + + + + + + + + <li class="entry even article" > + + + + + + + + + <div class="imgw"> + + + + <ul> + <li > + <a href="http://wyborcza.biz/biznes/7,147743,24314790,polskie-kasjerki-zaczynaja-sie-bac-kiedy-straca-prace-przez.html"> + <img src="https://bi.im-g.pl/im/3c/32/17/z24325436II,Kasa-samoobslugowa-w-drogerii-Rossmann-.jpg"/> + </a> + + + </li> + </ul> + + + </div> + + + + + + + + + + + + + + + + + + + + + + + + + + + <span class="base"><a href="http://wyborcza.biz/biznes/0,147743.html">Konsument i zakupy</a> + </span> + + + + + + + + + <h4> + <a title="Kasjerki i kasjerzy si boj. Kiedy strac prac przez automaty?" href="http://wyborcza.biz/biznes/7,147743,24314790,polskie-kasjerki-zaczynaja-sie-bac-kiedy-straca-prace-przez.html">Kasjerki i kasjerzy si boj. Kiedy strac prac przez automaty?</a> + </h4> + + + + + + + + + + + + + + + + </li> + + + + + + + + + + + + <li class="entry odd article" > + + + + + + + + + <div class="imgw"> + + + + <ul> + <li > + <a href="http://wyborcza.biz/biznes/7,147743,24323989,zywnosc-gaz-prad-paliwa-ceny-pietruszki-moga-byc-rekordowe.html"> + <img src="https://bi.im-g.pl/im/e3/32/17/z24324067II,Warzywa.jpg"/> + </a> + + + </li> + </ul> + + + </div> + + + + + + + + + + + + + + + + + + + + + + + + + + + <span class="base"><a href="http://wyborcza.biz/biznes/0,147743.html">Konsument i zakupy</a> + </span> + + + + + + + + + <h4> + <a title="ywno, gaz, prd, paliwa... Ceny pietruszki mog by rekordowe. Co jeszcze zdroeje w 2019? A co stanieje?" href="http://wyborcza.biz/biznes/7,147743,24323989,zywnosc-gaz-prad-paliwa-ceny-pietruszki-moga-byc-rekordowe.html">ywno, gaz, prd, paliwa... Ceny pietruszki mog by rekordowe. Co jeszcze zdroeje w 2019? A co stanieje?</a> + </h4> + + + + + + + + + + + + + + + + </li> + + + + + + + + + + + + <li class="entry even article" > + + + + + + + + + <div class="imgw"> + + + + <ul> + <li > + <a href="http://wyborcza.biz/biznes/7,147743,24303027,eksperyment-sklepowy-z-slodyczy-rezygnowac-nie-lubimy-przy.html"> + <img src="https://bi.im-g.pl/im/70/17/17/z24212592II.jpg"/> + </a> + + + </li> + </ul> + + + </div> + + + + + + + + + + + + + + + + + + + + + + + + + + + <span class="base"><a href="http://wyborcza.biz/biznes/0,147743.html">Konsument i zakupy</a> + </span> + + + + + + + + + <h4> + <a title="Ze sodyczy rezygnowa nie lubimy. Przy rybie robimy si asertywni. Na zakupach nie zawsze kierujemy si cen" href="http://wyborcza.biz/biznes/7,147743,24303027,eksperyment-sklepowy-z-slodyczy-rezygnowac-nie-lubimy-przy.html">Ze sodyczy rezygnowa nie lubimy. Przy rybie robimy si asertywni. Na zakupach nie zawsze kierujemy si cen</a> + </h4> + + + + + + + + + + + + + + + + </li> + + + + + + + + + + + + <li class="entry odd article" > + + + + + + + + + <div class="imgw"> + + + + <ul> + <li > + <a href="http://wyborcza.biz/biznes/7,147743,24293124,idzie-boom-kawowy-bedzie-taniej-i-zdrowiej.html"> + <img src="https://bi.im-g.pl/im/bf/0a/17/z24161983II.jpg"/> + </a> + + + </li> + </ul> + + + </div> + + + + + + + + + + + + + + + + + + + + + + + + + + + <span class="base"><a href="http://wyborcza.biz/biznes/0,147743.html">Konsument i zakupy</a> + </span> + + + + + + + + + <h4> + <a title="Idzie boom kawowy. Bdzie taniej i zdrowiej" href="http://wyborcza.biz/biznes/7,147743,24293124,idzie-boom-kawowy-bedzie-taniej-i-zdrowiej.html">Idzie boom kawowy. Bdzie taniej i zdrowiej</a> + </h4> + + + + + + + + + + + + + + + + </li> + + + + + + + </ul> + </div> + <div class="footer"> + + + + </div> +</div> +<!-- (~MODEND:7:21407481 s43a:1,313,063) --> + +<!--21407481, [ /tpl/prod/universalIndex/universalIndex.jsp ], universalIndexBean--> +
+
+
+ </div>
+ <section class="post-article-related">
+
+
+
+
+
+
+
+
+<!-- related debug: RelatedResultDto{relatedItems=[RelatedItem [xx=24414148, title=Duo smogu, mao alarmw. Polska "mistrzem" rakotwrczych wyzieww, url=http://wyborcza.pl/7,155287,24414148,europejski-raport-o-smogu-polska-walczy-za-slabo.html, photoUrl=https://bi.im-g.pl/im/3e/29/17/z24289086D.jpg, photoTitle=null], RelatedItem [xx=24376685, title=Miliony Polakw mog wreszcie odliczy od podatku panele soneczne. I... dalej dorzuca do kopciuchw, url=http://wyborcza.pl/7,155287,24376685,miliony-polakow-moga-wreszcie-odliczyc-od-podatku-panele-sloneczne.html, photoUrl=https://bi.im-g.pl/im/24/3f/17/z24376868D,Panele-fotowoltaiczne.jpg, photoTitle=Panele fotowoltaiczne], RelatedItem [xx=24349296, title=Kolejny milion uywanych aut z importu. Dodatkowa dostawa smogu z Zachodu, url=http://wyborcza.pl/7,155287,24349296,kolejny-milion-uzywanych-aut-z-importu-dodatkowa-dostawa-smogu.html, photoUrl=https://bi.im-g.pl/im/39/39/17/z24351545D,Uzywane-auta-z-Niemiec---w-drodze-do-polskiego-nab.jpg, photoTitle=Uywane auta z Niemiec - w drodze do polskiego nabywcy. A4, 29 stycznia 2017], RelatedItem [xx=24302580, title=Jaki oczyszczacz powietrza kupi do domu?, url=http://wyborcza.biz/pieniadzeekstra/7,134263,24302580,jaki-oczyszczacz-powietrza-kupic-do-domu.html, photoUrl=https://bi.im-g.pl/im/1f/2d/17/z24302879D,Oczyszczacz-powietrza.jpg, photoTitle=Oczyszczacz powietrza]], putBanPosition=2, articleAmount=4, itemsNoWithPutban=4} -->
+
+
+ <div class="gazeta_article_related_new">
+ <div class="rel_head" >
+ Zobacz take
+ </div>
+
+
+
+
+
+
+
+ <ul>
+
+
+ <li class="fLeft ">
+
+
+
+
+ <a href="http://wyborcza.pl/7,155287,24414148,europejski-raport-o-smogu-polska-walczy-za-slabo.html" title="Duo smogu, mao alarmw. Polska "mistrzem" rakotwrczych wyzieww">
+ <img src="https://bi.im-g.pl/im/3e/29/17/z24289086II.jpg" alt=""/>
+ </a>
+ <a class="t" href="http://wyborcza.pl/7,155287,24414148,europejski-raport-o-smogu-polska-walczy-za-slabo.html" title="Duo smogu, mao alarmw. Polska "mistrzem" rakotwrczych wyzieww" >Duo smogu, mao alarmw. Polska "mistrzem" rakotwrczych wyzieww</a>
+
+
+
+ </li>
+
+ <li class="">
+
+
+
+
+ <a href="http://wyborcza.pl/7,155287,24376685,miliony-polakow-moga-wreszcie-odliczyc-od-podatku-panele-sloneczne.html" title="Miliony Polakw mog wreszcie odliczy od podatku panele soneczne. I... dalej dorzuca do kopciuchw">
+ <img src="https://bi.im-g.pl/im/24/3f/17/z24376868II,Panele-fotowoltaiczne.jpg" alt="Panele fotowoltaiczne"/>
+ </a>
+ <a class="t" href="http://wyborcza.pl/7,155287,24376685,miliony-polakow-moga-wreszcie-odliczyc-od-podatku-panele-sloneczne.html" title="Miliony Polakw mog wreszcie odliczy od podatku panele soneczne. I... dalej dorzuca do kopciuchw" >Miliony Polakw mog wreszcie odliczy od podatku panele soneczne. I... dalej dorzuca do kopciuchw</a>
+
+
+
+ </li>
+
+ <li class="">
+
+
+
+
+
+
+
+
+
+
+
+
+ <script src='https://www.googletagservices.com/tag/js/gpt.js'>
+ var _YB = _YB || {
+ ab: function() {
+ return (Math.random() >= 0.1 ? 'b' : 'a' + Math.floor(Math.random() * 10));
+ }
+ };
+ var _yt = new Date(), yb_th = _yt.getUTCHours() - 8, yb_tm = _yt.getUTCMinutes(), yb_wd = _yt.getUTCDay();
+ if (yb_th < 0) {
+ yb_th = 24 + yb_th;
+ yb_wd -= 1;
+ };
+ if (yb_wd < 0) {
+ yb_wd = 7 + yb_wd
+ };
+ googletag.pubads().definePassback('/52555387/wyborcza.pl_native_desktop', ['fluid']).setTargeting('yb_ab', _YB.ab()).setTargeting('yb_ff', String(Math.round(Math.random()))).setTargeting('yb_th', yb_th.toString()).setTargeting('yb_tm', yb_tm.toString()).setTargeting('yb_wd', yb_wd.toString()).display();
+ </script>
+
+
+
+
+
+
+
+ </li>
+
+ <li class="">
+
+
+
+
+ <a href="http://wyborcza.pl/7,155287,24349296,kolejny-milion-uzywanych-aut-z-importu-dodatkowa-dostawa-smogu.html" title="Kolejny milion uywanych aut z importu. Dodatkowa dostawa smogu z Zachodu">
+ <img src="https://bi.im-g.pl/im/39/39/17/z24351545II,Uzywane-auta-z-Niemiec---w-drodze-do-polskiego-nab.jpg" alt="Uywane auta z Niemiec - w drodze do polskiego nabywcy. A4, 29 stycznia 2017"/>
+ </a>
+ <a class="t" href="http://wyborcza.pl/7,155287,24349296,kolejny-milion-uzywanych-aut-z-importu-dodatkowa-dostawa-smogu.html" title="Kolejny milion uywanych aut z importu. Dodatkowa dostawa smogu z Zachodu" >Kolejny milion uywanych aut z importu. Dodatkowa dostawa smogu z Zachodu</a>
+
+
+
+ </li>
+
+
+ </ul>
+ </div>
+
+
+
+<!-- /relatedModule v 2.0 -->
+
+ +<!--9638939, [ /fix/modules/wyborcza/relatedModule_dfp.jsp ], relatedArticlesModule--> +
+ </section>
+ <div class="post-article-content-holder column-48" data-position="48,1-5 12-15">
+
+
+
+
+
+
+
+
+
+ </div>
+
+
+
+
+
+
+ <div id="tags" class="tags">
+ <span class="tags-label">Wicej na ten temat:</span>
+
+ <a href="/biznes/0,104259.html?tag=smog" title="smog" class="tag-anchor"><span class="tag-label">smog</span></a>,
+
+ <a href="/biznes/0,104259.html?tag=w%EAgiel" title="wgiel" class="tag-anchor"><span class="tag-label">wgiel</span></a>,
+
+ <a href="/biznes/0,104259.html?tag=jako%B6%E6+powietrza" title="jako powietrza" class="tag-anchor"><span class="tag-label">jako powietrza</span></a>,
+
+ <a href="/biznes/0,104259.html?tag=zanieczyszczenie+powietrza" title="zanieczyszczenie powietrza" class="tag-anchor"><span class="tag-label">zanieczyszczenie powietrza</span></a>
+
+ </div>
+
+
+<!-- universalTags v1.0 -->
+ +<!--410352220, [ /fix/modules/wyborcza/universalTags.jsp ], seoTagController--> +
+ </section>
+
+ <div class="social">
+ <div class="social-bar">
+
+
+
+
+
+
+
+ <div class="socialBar">
+ <ul>
+ <li class="readLater">
+ <a href="#" title="Dodaj do schowka">
+ <figure class="badge-ico">
+ <svg class="badge-symbol">
+ <use xlink:href="#read-later"></use>
+ </svg>
+ </figure>
+ </a>
+ <span class="sharesCount"></span>
+ </li>
+
+
+ <li class="fbShare">
+ <a href="#" title="Udostpnij na Facebooku">
+ <figure class="badge-ico">
+ <svg class="badge-symbol">
+ <use xlink:href="#face-v2"></use>
+ </svg>
+ </figure>
+ </a>
+ <span class="sharesCount"></span>
+ </li>
+
+
+
+ <li class="twitter">
+ <a href="#" title="Udostpnij na Twitterze">
+ <figure class="badge-ico">
+ <svg class="badge-symbol">
+ <use xlink:href="#twitter-v2"></use>
+ </svg>
+ </figure>
+ </a>
+ <span class="tweetsCount"></span>
+ </li>
+
+
+
+
+ <li class="email">
+ <a href="http://gazeta.hit.gemius.pl/hitredir/id=nF6a2AyFf_IJ3woQ3DRcKcewPxLw1u7SUeW3YDM9WRb.P7/stparam=wkionrdgis/url=http://wyborcza.biz/2029030,75247.html?b=1&x=24417936&d=0&utm_source=recommend&utm_medium=email&utm_campaign=recommend&home=http://wyborcza.biz/biznes/7,147743,24417936,pomysl-na-biznes-chusta-ktora-chroni-przed-smogiem.html?disableRedirects=true" title="Udostpnij e-mailem">
+ <figure class="badge-ico">
+ <svg class="badge-symbol">
+ <use xlink:href="#mail-v2"></use>
+ </svg>
+ </figure>
+ </a>
+ </li>
+
+
+
+ <li class="comments">
+ <a href="#" title="Zobacz komentarze">
+ <figure class="badge-ico">
+ <svg class="badge-symbol">
+ <use xlink:href="#comments-v2"></use>
+ </svg>
+ </figure>
+ </a>
+ <span class="commentsCount"></span>
+ </li>
+
+
+ </ul>
+ </div>
+
+
+<!-- socialBar v1.1 -->
+ +<!--9638922, [ /fix/modules/wyborcza/socialBar.jsp ], toolsSocialMediaPojoBean--> +
+ </div>
+ <div class="comments-container">
+
+
+
+
+
+
+
+<!-- opinions20 -->
+<!-- serwis: 2 -->
+<!-- role: -->
+
+
+
+
+
+
+
+
+
+<script>
+ wyborcza_pl.commentsUserData = Object.freeze((function () {
+ var opts = {
+ roles: {
+ admin: false,
+ editor: false
+ },
+ userNick: "",
+ loginChannel: '',
+ userLink: '',
+ wyborcza_pl_service_id: 2,
+ voting: true,
+ writing: false,
+ commentedObjectId: 24417936
+ };
+
+ function getParam(paramName) {
+ return opts[paramName];
+ }
+
+ function getRole(roleName) {
+ return opts['roles'][roleName];
+ }
+
+ return {
+ getParam: getParam,
+ isRole: getRole
+ };
+
+ })());
+</script>
+
+
+
+ <!-- komentarze ON -->
+ <!-- test session: '' / '' : '' : '' -->
+
+
+ <section class="comments">
+
+
+
+ <header class="oHead">
+ <div class="oCntInf">
+ <span class="ocntHead">Komentarze</span>
+ <div class="oCntWrap">
+ <svg class="oCntBg">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#comment"></use>
+ </svg>
+ <span id="oCnt"></span>
+ </div>
+ </div>
+
+ <!-- komentarze_pisanie: 'false' -->
+ <div class="oFormBox">
+ <form action="#" method="post" class="oForm">
+ <div class="row rTop">
+
+
+
+
+ <div class="subscribeToWriteComment">Chcesz doczy do dyskusji? <a href="//wyborcza.pl/komentarzeoferta">Zosta naszym prenumeratorem</a></div>
+
+
+ </div>
+
+ <div class="row rBtm">
+
+
+ <div class="cLogin">
+ <span class="cLoginLink">Zaloguj si</span>
+ <div class="logOpts">
+
+ <a data-action-login="wyborcza" id="oLoginWyborcza" title="Zaloguj uywajc konta Wyborcza">
+ <svg xmlns="http://www.w3.org/2000/svg" class="logOptsWybprcza" viewBox="0 0 28 28">
+ <defs>
+ <style>
+ #wyborcza_PL_rect1 { fill: #fff; } #wyborcza_PL_rect2, #wyborcza_PL_polygon1 { fill: #a1a1a1; } #wyborcza_PL:hover #wyborcza_PL_polygon1 { fill: #000; } #wyborcza_PL:hover #wyborcza_PL_rect1 { fill: #f1f1f1; } #wyborcza_PL:hover #wyborcza_PL_rect2 { fill: #df012d; }
+ </style>
+ </defs>
+ <g id="wyborcza_PL">
+ <rect id="wyborcza_PL_rect1" width="28" height="28"/>
+ <rect id="wyborcza_PL_rect2" width="12.22" height="18.84" x="3.56" y="4.58"/>
+ <polygon id="wyborcza_PL_polygon1" points="26.73 10.38 25.81 10.38 24.16 16.62 24.03 16.61 22.14 11.2 23.26 10.54 23.26 10.38 18.55 10.38 18.55 10.54 19.29 11.21 22.25 19.53 23.95 19.53 25.31 14.44 25.45 14.44 27.25 19.53 28 19.53 28 14.03 26.73 10.38"/>
+ </g>
+ </svg>
+ </a>
+
+ <a data-action-login="facebook" id="oLoginFacebook" title="Zaloguj uywajc Facebooka">
+ <svg xmlns="http://www.w3.org/2000/svg" class="logOptsFacebook" viewBox="0 0 28 28">
+ <defs>
+ <style>
+ #Facebook_Ico #Facebook_Ico_path1 { fill: #fff; fill-rule: evenodd; } #Facebook_Ico:hover #Facebook_Ico_path1 { fill: #3664a2; fill-rule: evenodd; } #Facebook_Ico #Facebook_Ico_rect1 { fill: #a1a1a1; } #Facebook_Ico:hover #Facebook_Ico_rect1 { fill: #fff; }
+ </style>
+ </defs>
+ <g id="Facebook_Ico">
+ <rect id="Facebook_Ico_rect1" width="28" height="28"/>
+ <path id="Facebook_Ico_path1" d="M-9.28-4.3v28h28v-28h-28ZM9,9.28H5.73v9.42H2.21V9.27H-0.29V7.14H2.22V4.41c0-4,4.42-3.69,4.42-3.69h2.6V3.29H7.14A1.17,1.17,0,0,0,5.73,4.35V7.14h4Z" transform="translate(9.28 4.3)"/>
+ </g>
+ </svg>
+ </a>
+
+ <a data-action-login="google" id="oLoginGoogle" title="Zaloguj uywajc Google+">
+ <svg xmlns="http://www.w3.org/2000/svg" class="logOptsFacebook" viewBox="0 0 28 28">
+ <defs>
+ <style>
+ #GooglePlus_Ico #GooglePlus_Ico_path1 { fill: #fff; fill-rule: evenodd; } #GooglePlus_Ico:hover #GooglePlus_Ico_path1 { fill-rule: evenodd; fill: #d34836; } #GooglePlus_Ico:hover #GooglePlus_Ico_rect1 { fill: #fff; } #GooglePlus_Ico #GooglePlus_Ico_rect1 { fill: #a1a1a1; }
+ </style>
+ </defs>
+ <g id="GooglePlus_Ico">
+ <rect id="GooglePlus_Ico_rect1" width="28" height="28"/>
+ <path id="GooglePlus_Ico_path1" d="M0,0V28H28V0H0ZM10.36,20A6.18,6.18,0,0,1,4,14a6.18,6.18,0,0,1,6.36-6,6.31,6.31,0,0,1,4.26,1.57L12.9,11.14a3.7,3.7,0,0,0-2.54-.93A3.87,3.87,0,0,0,6.42,14a3.87,3.87,0,0,0,3.95,3.79A3.39,3.39,0,0,0,14,15.2H10.36V13.14h6a5.23,5.23,0,0,1,.1,1C16.47,17.57,14,20,10.36,20ZM24,14.86H22.18v1.71H20.36V14.86H18.55V13.14h1.82V11.43h1.82v1.71H24v1.71Z"/>
+ </g>
+ </svg>
+ </a>
+
+ </div>
+
+
+
+ </div>
+
+
+
+
+
+
+ <div class="cSubmit">
+ <button id="oFormSubmit" class="sendCommentButton" disabled="disabled" name="submit">Skomentuj</button>
+ </div>
+ </div>
+ </form>
+ </div>
+
+ <!-- komentarze_sortowanie: PopularityDesc -->
+ <div class="oSortOpts" data-sort="Popularity-Desc">
+ <a data-action="sortComments" data-sort-by="Time">Najnowsze</a>
+ <a data-action="sortComments" data-sort-by="Popularity">Popularne</a>
+ </div>
+ </header><!-- /header -->
+
+
+
+
+
+
+ <!-- komentarze_liczba_pod_art: 8 -->
+
+ <section class="oBody">
+
+
+ <!-- komentarz -->
+
+ <div class="cRow cResHidden flag_PUBLISHED" id="opinion101252015" itemscope
+ itemtype="http://schema.org/Comment" itemid="#opinion:101252015">
+ <div class="cHead">
+
+ <span itemprop="author" itemscope itemtype="http://schema.org/Person"><span class="cName" itemprop="name">
+
+
+
+ sceptyczny_debunk
+
+
+ </span></span>
+
+ <span class="cDate" itemprop="datePublished" content="2019-01-31">31.01.2019, 16:16</span>
+
+
+ </div>
+ <div class="cBody">
+
+
+
+ <span itemprop="text">W Azji maski nosi si nie z powodu smogu, tylko SARS, ptasiej grypy i z powodu przepisw sanitarnych, ktre nakazuj nosi maski w miejscach publicznych osobom kaszlcym czy kichajcym. Z tego samego powodu w wielu miejscach publicznych s montowane kamery mierzce temperatur ciaa przechodniw, np. w metrze w Hongkongu czy w Tajpej. Jak si patrzy na polski autobus w styczniu peen zasmarkanych, kichajcych i kaszlcych ludzi, ktrzy nawet doni nie zasaniaj ust, to trzeba si zastanowi, jakim cudem ten nard jeszcze nie wymar.</span></div>
+ <div class="cFt">
+
+ <!-- komentarze_ocenianie ON -->
+ <div class="cAlreadyVoted"><span class="cVotedInfo">ju oceniae(a)</span></div>
+ <div class="cVoteUp">
+ <a data-action="vote-addPlus" data-post-id="101252015">
+ <svg class="voteUpIcon">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#vote-up"></use>
+ </svg>
+ </a>
+ <span itemprop="upvoteCount">9</span>
+ </div>
+
+ <div class="cVoteDown">
+ <a data-action="vote-addMinus" data-post-id="101252015">
+ <svg class="voteDownIcon">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#vote-down"></use>
+ </svg>
+ </a>
+ <span itemprop="downvoteCount">1</span>
+ </div>
+
+ <div class="cVoteSpam">
+ <a title="Zgo do moderacji" data-action="trash-vote" data-post-id="101252015">
+ <svg class="voteSpamIcon">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#spam"></use>
+ </svg>
+ </a>
+ </div>
+ <div class="cResWrite">
+ <a data-child-for="101252015" data-reply-to="sceptyczny_debunk">
+ <svg class="resWriteIcon">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#reply"></use>
+ </svg>
+ Odpowiedz</a>
+ </div>
+
+
+ <div class="cResShow">
+ <a data-res-counter="1" data-state="show">
+ <svg class="resShowIcon">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#comment"></use>
+ </svg>
+
+
+
+ Poka odpowiedzi (1)
+
+
+ </a>
+ </div>
+
+ </div>
+
+
+
+ <!-- Odpowiedzi na komentarz -->
+
+ <!-- komentarz zagniezdzony -->
+ <div class="cRow cResComment flag_PUBLISHED" id="opinion101252118"
+ itemscope itemtype="http://schema.org/Comment" itemid="#opinion:101252118">
+ <div class="cHead">
+
+ <span itemprop="author" itemscope itemtype="http://schema.org/Person"><span class="cName" itemprop="name">
+
+
+
+ lizabet
+
+ </span></span>
+
+ <span class="cDate" itemprop="datePublished"
+ content="2019-01-31">31.01.2019, 16:39</span>
+
+
+ </div>
+ <div class="cBody">
+
+
+
+ <span itemprop="text">@sceptyczny_debunk<br/>Czsto o tym myl. Dziki za te celne uwagi.</span></div>
+ <div class="cFt">
+
+ <!-- komentarze_ocenianie ON -->
+ <div class="cAlreadyVoted"><span class="cVotedInfo">ju oceniae(a)</span></div>
+ <div class="cVoteUp">
+ <a data-action="vote-addPlus" data-post-id="101252118">
+ <svg class="voteUpIcon">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#vote-up"></use>
+ </svg>
+ </a>
+ <span itemprop="upvoteCount">1</span>
+ </div>
+
+ <div class="cVoteDown">
+ <a data-action="vote-addMinus" data-post-id="101252118">
+ <svg class="voteDownIcon">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#vote-down"></use>
+ </svg>
+ </a>
+ <span itemprop="downvoteCount">0</span>
+ </div>
+
+ <div class="cVoteSpam">
+ <a title="Zgo do moderacji" data-action="trash-vote" data-post-id="101252118">
+ <svg class="voteSpamIcon">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#spam"></use>
+ </svg>
+ </a>
+ </div>
+ <div class="cResWrite">
+ <a data-child-for="101252015" data-reply-to="lizabet">
+ <svg class="resWriteIcon">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#reply"></use>
+ </svg>
+ Odpowiedz</a>
+ </div>
+ </div>
+ </div>
+ <!-- / komentarz zagniezdzony -->
+
+
+
+
+
+ </div>
+ <!-- / komentarz -->
+
+
+
+ <!-- komentarz -->
+
+ <div class="cRow cResHidden flag_PUBLISHED" id="opinion101252057" itemscope
+ itemtype="http://schema.org/Comment" itemid="#opinion:101252057">
+ <div class="cHead">
+
+ <span itemprop="author" itemscope itemtype="http://schema.org/Person"><span class="cName" itemprop="name">
+
+
+
+ tegonielubie
+
+
+ </span></span>
+
+ <span class="cDate" itemprop="datePublished" content="2019-01-31">31.01.2019, 16:25</span>
+
+
+ </div>
+ <div class="cBody">
+
+
+
+ <span itemprop="text">cena - 65 euro</span></div>
+ <div class="cFt">
+
+ <!-- komentarze_ocenianie ON -->
+ <div class="cAlreadyVoted"><span class="cVotedInfo">ju oceniae(a)</span></div>
+ <div class="cVoteUp">
+ <a data-action="vote-addPlus" data-post-id="101252057">
+ <svg class="voteUpIcon">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#vote-up"></use>
+ </svg>
+ </a>
+ <span itemprop="upvoteCount">2</span>
+ </div>
+
+ <div class="cVoteDown">
+ <a data-action="vote-addMinus" data-post-id="101252057">
+ <svg class="voteDownIcon">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#vote-down"></use>
+ </svg>
+ </a>
+ <span itemprop="downvoteCount">1</span>
+ </div>
+
+ <div class="cVoteSpam">
+ <a title="Zgo do moderacji" data-action="trash-vote" data-post-id="101252057">
+ <svg class="voteSpamIcon">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#spam"></use>
+ </svg>
+ </a>
+ </div>
+ <div class="cResWrite">
+ <a data-child-for="101252057" data-reply-to="tegonielubie">
+ <svg class="resWriteIcon">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#reply"></use>
+ </svg>
+ Odpowiedz</a>
+ </div>
+
+
+ </div>
+
+
+
+
+
+ </div>
+ <!-- / komentarz -->
+
+
+
+ <!-- komentarz -->
+
+ <div class="cRow cResHidden flag_PUBLISHED" id="opinion101251975" itemscope
+ itemtype="http://schema.org/Comment" itemid="#opinion:101251975">
+ <div class="cHead">
+
+ <span itemprop="author" itemscope itemtype="http://schema.org/Person"><span class="cName" itemprop="name">
+
+
+
+ baza
+
+
+ </span></span>
+
+ <span class="cDate" itemprop="datePublished" content="2019-01-31">31.01.2019, 16:06</span>
+
+
+ </div>
+ <div class="cBody">
+
+
+
+ <span itemprop="text">Zwyky wentylator nakryj filtrem wglowym i w domu oczyci powietrze.</span></div>
+ <div class="cFt">
+
+ <!-- komentarze_ocenianie ON -->
+ <div class="cAlreadyVoted"><span class="cVotedInfo">ju oceniae(a)</span></div>
+ <div class="cVoteUp">
+ <a data-action="vote-addPlus" data-post-id="101251975">
+ <svg class="voteUpIcon">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#vote-up"></use>
+ </svg>
+ </a>
+ <span itemprop="upvoteCount">1</span>
+ </div>
+
+ <div class="cVoteDown">
+ <a data-action="vote-addMinus" data-post-id="101251975">
+ <svg class="voteDownIcon">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#vote-down"></use>
+ </svg>
+ </a>
+ <span itemprop="downvoteCount">1</span>
+ </div>
+
+ <div class="cVoteSpam">
+ <a title="Zgo do moderacji" data-action="trash-vote" data-post-id="101251975">
+ <svg class="voteSpamIcon">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#spam"></use>
+ </svg>
+ </a>
+ </div>
+ <div class="cResWrite">
+ <a data-child-for="101251975" data-reply-to="baza">
+ <svg class="resWriteIcon">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#reply"></use>
+ </svg>
+ Odpowiedz</a>
+ </div>
+
+
+ <div class="cResShow">
+ <a data-res-counter="2" data-state="show">
+ <svg class="resShowIcon">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#comment"></use>
+ </svg>
+
+
+
+ Poka odpowiedzi (2)
+
+
+ </a>
+ </div>
+
+ </div>
+
+
+
+ <!-- Odpowiedzi na komentarz -->
+
+ <!-- komentarz zagniezdzony -->
+ <div class="cRow cResComment flag_PUBLISHED" id="opinion101252012"
+ itemscope itemtype="http://schema.org/Comment" itemid="#opinion:101252012">
+ <div class="cHead">
+
+ <span itemprop="author" itemscope itemtype="http://schema.org/Person"><span class="cName" itemprop="name">
+
+
+
+ vocativus
+
+ </span></span>
+
+ <span class="cDate" itemprop="datePublished"
+ content="2019-01-31">31.01.2019, 16:15</span>
+
+
+ </div>
+ <div class="cBody">
+
+
+
+ <span itemprop="text">@baza<br/>oczyci nieznacznie z substancji lotnych, ale nie z pyw</span></div>
+ <div class="cFt">
+
+ <!-- komentarze_ocenianie ON -->
+ <div class="cAlreadyVoted"><span class="cVotedInfo">ju oceniae(a)</span></div>
+ <div class="cVoteUp">
+ <a data-action="vote-addPlus" data-post-id="101252012">
+ <svg class="voteUpIcon">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#vote-up"></use>
+ </svg>
+ </a>
+ <span itemprop="upvoteCount">0</span>
+ </div>
+
+ <div class="cVoteDown">
+ <a data-action="vote-addMinus" data-post-id="101252012">
+ <svg class="voteDownIcon">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#vote-down"></use>
+ </svg>
+ </a>
+ <span itemprop="downvoteCount">0</span>
+ </div>
+
+ <div class="cVoteSpam">
+ <a title="Zgo do moderacji" data-action="trash-vote" data-post-id="101252012">
+ <svg class="voteSpamIcon">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#spam"></use>
+ </svg>
+ </a>
+ </div>
+ <div class="cResWrite">
+ <a data-child-for="101251975" data-reply-to="vocativus">
+ <svg class="resWriteIcon">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#reply"></use>
+ </svg>
+ Odpowiedz</a>
+ </div>
+ </div>
+ </div>
+ <!-- / komentarz zagniezdzony -->
+
+ <!-- komentarz zagniezdzony -->
+ <div class="cRow cResComment flag_PUBLISHED" id="opinion101252134"
+ itemscope itemtype="http://schema.org/Comment" itemid="#opinion:101252134">
+ <div class="cHead">
+
+ <span itemprop="author" itemscope itemtype="http://schema.org/Person"><span class="cName" itemprop="name">
+
+
+
+ Ardalis
+
+ </span></span>
+
+ <span class="cDate" itemprop="datePublished"
+ content="2019-01-31">31.01.2019, 16:44</span>
+
+
+ </div>
+ <div class="cBody">
+
+
+
+ <span itemprop="text">@baza<br/>Filtr wglowy nie suy do zatrzymywania pyw. Jeeli sam chcesz zrobi filtr smogu, czyli czstek okoo 1 mikrona potrzebujesz wkniny min klasy F9 a lepiej E10-11. Wtedy niestety wzrastaj opory przepywu i bdziesz potrzebowa wentylatora nieco wyszych cinie, np limakowego. Wszystko to oczywicie trzeba uszczelini. Taniej moe by jednak kupi gotowy.</span></div>
+ <div class="cFt">
+
+ <!-- komentarze_ocenianie ON -->
+ <div class="cAlreadyVoted"><span class="cVotedInfo">ju oceniae(a)</span></div>
+ <div class="cVoteUp">
+ <a data-action="vote-addPlus" data-post-id="101252134">
+ <svg class="voteUpIcon">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#vote-up"></use>
+ </svg>
+ </a>
+ <span itemprop="upvoteCount">1</span>
+ </div>
+
+ <div class="cVoteDown">
+ <a data-action="vote-addMinus" data-post-id="101252134">
+ <svg class="voteDownIcon">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#vote-down"></use>
+ </svg>
+ </a>
+ <span itemprop="downvoteCount">0</span>
+ </div>
+
+ <div class="cVoteSpam">
+ <a title="Zgo do moderacji" data-action="trash-vote" data-post-id="101252134">
+ <svg class="voteSpamIcon">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#spam"></use>
+ </svg>
+ </a>
+ </div>
+ <div class="cResWrite">
+ <a data-child-for="101251975" data-reply-to="Ardalis">
+ <svg class="resWriteIcon">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#reply"></use>
+ </svg>
+ Odpowiedz</a>
+ </div>
+ </div>
+ </div>
+ <!-- / komentarz zagniezdzony -->
+
+
+
+
+
+ </div>
+ <!-- / komentarz -->
+
+
+ </section>
+ <!-- /.oBody -->
+
+
+ <footer class="oFooter">
+ <div class="oFooterContainer">
+ <p class="oFooterParagraph">
+ <a data-action="loadAllComments" title="Zobacz wicej" class="oMoreBtn">Wicej</a>
+ </p>
+ </div>
+ </footer>
+ <!-- /.oFooter -->
+
+ </section>
+ <!-- / KOMENTARZE -->
+
+
+<img id="activity-check" src="/fix/modules/wyborcza/comments/activity.jsp">
+ +<!--9638926, [ /fix/modules/opinions20.jsp ], opinions20PojoBean--> +
+ </div>
+ </div>
+
+ <section class="complementary-entries">
+ <div class="first-bottom-content-holder column-54" data-position="54,12-15">
+
+
+
+
+ </div>
+ </section>
+ </section>
+
+
+ <aside class="article-extra grid-col-1-4 sidebar">
+ <div class="right-content-holder column-5 splint-glow" data-position="5,1-5 12-15">
+ <div class="mcBan" id="newsletterDM"></div>
+
+ <!--n.cz., tr:#TRNajCzytSST--><article class="mod mod_most_read mod_most_read1 stare-najczesciej-czytane"><header>Najczciej Czytane</header><section class="body"><ul><li class=""><span class="number">1.</span><h3 class="title"><a href="http://wyborcza.biz/biznes/1,147758,14592095,Pokazali_apartamenty_w_Zlotej_44__Cena__65_tys__za.html#TRNajCzytSST" title="Pokazali apartamenty w Zotej 44. Cena: 65 tys. za metr">Pokazali apartamenty w Zotej 44. Cena: 65 tys. za metr</a></h3></li><li class=""><span class="number">2.</span><h3 class="title"><a href="http://wyborcza.biz/biznes/1,147758,12670651,Najwezszy_dom_swiata_prawie_gotowy__ZDJECIA_.html#TRNajCzytSST" title="Najwszy dom wiata prawie gotowy [ZDJCIA]">Najwszy dom wiata prawie gotowy [ZDJCIA]</a></h3></li><li class=""><span class="number">3.</span><h3 class="title"><a href="http://wyborcza.biz/biznes/7,147758,24397604,galopujace-ceny-mieszkan-zderza-sie-ze-sciana-eksperci-podpowiadaja.html#TRNajCzytSST" title="Deweloperzy buduj coraz drosze mieszkania. Klienci kupuj nawet dziury w ziemi">Deweloperzy buduj coraz drosze mieszkania. Klienci kupuj nawet dziury w ziemi</a></h3></li><li class=""><span class="number">4.</span><h3 class="title"><a href="http://wyborcza.biz/biznes/1,147582,18379055,najwieksze-ofiary-amber-gold-kto-stracil-miliony-w-parabanku.html#TRNajCzytSST" title="Najwiksze ofiary Amber Gold. Sprawdzilimy, kto straci miliony w parabanku">Najwiksze ofiary Amber Gold. Sprawdzilimy, kto straci miliony w parabanku</a></h3></li><li class=""><span class="number">5.</span><h3 class="title"><a href="http://wyborcza.biz/biznes/1,147758,17989403,Najlepszy_projekt_wnetrza_biurowca_na_wodzie.html#TRNajCzytSST" title="Najlepszy projekt wntrza biurowca na wodzie">Najlepszy projekt wntrza biurowca na wodzie</a></h3></li><li class=""><span class="number">6.</span><h3 class="title"><a href="http://wyborcza.biz/biznes/7,147743,22836294,ile-kosztuja-wlosy-nawet-8-tys-zl-za-kg-tyle-mozna-zarobic.html#TRNajCzytSST" title="Ile kosztuj wosy? Nawet 8 tys. z za 1 kg. Tyle mona zarobi ">Ile kosztuj wosy? Nawet 8 tys. z za 1 kg. Tyle mona zarobi </a></h3></li></ul></section></article> +<!--21406650, [ /htm/21406/j21406650.htm ], null--> +
+
+ <!-- reklama -->
+ <div id="rectangle-splint" class="splint-element">
+
+
+
+<div id="003-RECTANGLE" class="adviewDFPBanner DFP-003-RECTANGLE">
+ <span class="banLabel" style="display: none;">REKLAMA</span>
+ <div id='div-gpt-ad-003-RECTANGLE-0'>
+ <script type='text/javascript'>
+ if(dfpParams.slots['003-RECTANGLE'] && dfpParams.slots['003-RECTANGLE'].autoLoad) {
+ if (adviewDFP && adviewDFP.scrollSlot) adviewDFP.scrollSlot.push('003-RECTANGLE');
+ } else {
+ googletag.cmd.push(function() { googletag.display('div-gpt-ad-003-RECTANGLE-0'); });
+ }
+
+ </script>
+ </div>
+</div>
+
+
+<!-- v2.2 --> +<!--10185261, [ /tpl/ads/prod/dfpSlot.jsp ], dfpBanersSlotBean--> +
+ </div>
+
+ <section class="mod-generic mod-generic-type-b-no-paddings mod-generic-type-b box-cor mod-opinions top-margin-layout la-http://wyborcza.pl/0,75968.html#TRNavSST"> + +<ul class="mod-generic-items"> +<li class="mod-generic-item +mod-generic-item-other-with-photo +"> +<div class="mod-generic-item-content"> +<figure class="mod-generic-item-photo-wrapper"> +<a id="LinkArea:http://wyborcza.pl/0,75968.html#TRNavSST" href="http://wyborcza.biz/biznes/7,157729,24377175,kolejne-branze-na-celowniku-fiskusa.html" class="mod-generic-item-photo-wrapper" title="Kolejne brane na celowniku fiskusa Budowlanka, gastronomia, motoryzacja – objte specjalnym nadzorem fiskalnym"> +<img class="mod-generic-item-photo-wrapper-image" data-src="//bi.im-g.pl/im/e5/3f/17/z24377317J.jpg" alt="Kolejne brane na celowniku fiskusa Budowlanka, gastronomia, motoryzacja – objte specjalnym nadzorem fiskalnym" /> +</a> +</figure> +<span class="rekl-text">Materiay promocyjne partnera</span> +<a class="mod-generic-title-container" id="LinkArea:http://wyborcza.pl/0,75968.html#TRNavSST" href="http://wyborcza.biz/biznes/7,157729,24377175,kolejne-branze-na-celowniku-fiskusa.html" title="Kolejne brane na celowniku fiskusa Budowlanka, gastronomia, motoryzacja – objte specjalnym nadzorem fiskalnym"> +<span class="mod-generic-title">Kolejne brane na celowniku fiskusa Budowlanka, gastronomia, motoryzacja – objte specjalnym nadzorem fiskalnym</span> +</a> +</div> +</li> +</ul> +</section><!-- UZREditor --><!-- htmEOF --> +<!--22243333, [ /htm/22243/j22243333.htm ], null--> +
+
+ <div id="rectangle-splint-067" class="splint-element-small">
+
+
+
+<div id="067-RECTANGLE-BTF" class="adviewDFPBanner DFP-067-RECTANGLE-BTF">
+ <span class="banLabel" style="display: none;">REKLAMA</span>
+ <div id='div-gpt-ad-067-RECTANGLE-BTF-0'>
+ <script type='text/javascript'>
+ if(dfpParams.slots['067-RECTANGLE-BTF'] && dfpParams.slots['067-RECTANGLE-BTF'].autoLoad) {
+ if (adviewDFP && adviewDFP.scrollSlot) adviewDFP.scrollSlot.push('067-RECTANGLE-BTF');
+ } else {
+ googletag.cmd.push(function() { googletag.display('div-gpt-ad-067-RECTANGLE-BTF-0'); });
+ }
+
+ </script>
+ </div>
+</div>
+
+
+<!-- v2.2 --> +<!--11455919, [ /tpl/ads/prod/dfpSlot.jsp ], dfpBanersSlotBean--> +
+ </div>
+
+
+
+ <!-- reklama -->
+ <div id="rectangle-splint-035" class="splint-element-small">
+
+
+
+<div id="035-RECTANGLE-BTF" class="adviewDFPBanner DFP-035-RECTANGLE-BTF">
+ <span class="banLabel" style="display: none;">REKLAMA</span>
+ <div id='div-gpt-ad-035-RECTANGLE-BTF-0'>
+ <script type='text/javascript'>
+ if(dfpParams.slots['035-RECTANGLE-BTF'] && dfpParams.slots['035-RECTANGLE-BTF'].autoLoad) {
+ if (adviewDFP && adviewDFP.scrollSlot) adviewDFP.scrollSlot.push('035-RECTANGLE-BTF');
+ } else {
+ googletag.cmd.push(function() { googletag.display('div-gpt-ad-035-RECTANGLE-BTF-0'); });
+ }
+
+ </script>
+ </div>
+</div>
+
+
+<!-- v2.2 --> +<!--10185263, [ /tpl/ads/prod/dfpSlot.jsp ], dfpBanersSlotBean--> +
+ </div>
+
+
+
+ </div>
+ </aside>
+
+
+
+
+
+ </div>
+ </main>
+
+
+
+
+ <section class="ads container-outer">
+ <div class="container-inner">
+ <div class="grid-row">
+ <div class="grid-col-wide">
+
+
+
+<div id="042-FOOTBOARD" class="adviewDFPBanner DFP-042-FOOTBOARD">
+ <span class="banLabel" style="display: none;">REKLAMA</span>
+ <div id='div-gpt-ad-042-FOOTBOARD-0'>
+ <script type='text/javascript'>
+ if(dfpParams.slots['042-FOOTBOARD'] && dfpParams.slots['042-FOOTBOARD'].autoLoad) {
+ if (adviewDFP && adviewDFP.scrollSlot) adviewDFP.scrollSlot.push('042-FOOTBOARD');
+ } else {
+ googletag.cmd.push(function() { googletag.display('div-gpt-ad-042-FOOTBOARD-0'); });
+ }
+
+ </script>
+ </div>
+</div>
+
+
+<!-- v2.2 --> +<!--10185264, [ /tpl/ads/prod/dfpSlot.jsp ], dfpBanersSlotBean--> +
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <div class="container-inner">
+
+ <div class="grid-row">
+ <div class="grid-col-3-4">
+ <section class="complementary-entries">
+ <div class="second-bottom-content-holder column-57" data-position="57,1-5 12-15">
+
+
+
+
+
+
+
+
+
+ </div>
+ </section>
+ </div>
+ </div>
+
+
+
+ <div class="grid-row">
+ <div class="grid-col-4-4 column-61" data-position="61,1-15">
+ <div class="karaluch"> +<header> +<span>Wiadomoci - najwaniejsze informacje</span> +<i class="l"></i> +<i class="r"></i> +</header> +<section> +<header> +<span>Wiadomoci</span> +</header> +<ul> +<li><a href="http://wyborcza.pl/TylkoZdrowie/7,137474,23080156,dash-najzdrowsza-dieta-swiata.html" title="Dash: najzdrowsza dieta">Dash: najzdrowsza dieta</a></li><li><a href="http://biqdata.wyborcza.pl/biqdata/7,159116,23102688,ta-pierwsza-niehandlowa-niedziela-czyli-kiedy-nie-zrobisz-zakupow.html" title="Zakaz handlu w niedziele: kalendarz">Zakaz handlu w niedziele: kalendarz</a></li><li><a href="http://cojestgrane24.wyborcza.pl/cjg24/1,42.html" title="Muzea i galerie">Muzea i galerie</a></li><li><a href="http://wyborcza.pl/7,155287,22875272,teresa-czerwinska-bezpartyjny-fachowiec-kim-jest-nowa-ministra.html" title="Teresa Czerwiska">Teresa Czerwiska</a></li><li><a href="http://wyborcza.pl/7,75398,22876010,deklaracja-wiary-prof-szumowskiego-nowy-minister-zdrowia-wrogiem.html" title="ukasz Szumowski">ukasz Szumowski</a></li><li><a href="http://wyborcza.pl/7,155287,22873972,andrzej-adamczyk-minister-infrastruktury.html" title="Andrzej Adamczyk">Andrzej Adamczyk</a></li><li><a href="http://wyborcza.pl/7,155287,22874245,jadwiga-emilewicz-kim-jest-nowa-ministra-przedsiebiorczosci.html" title="Jadwiga Emilewicz">Jadwiga Emilewicz</a></li><li><a href="http://wyborcza.pl/7,155287,22875049,henryk-kowalczyk-szara-eminencja-pis-odpowiada-teraz-za-lasy.html" title="Henryk Kowalczyk">Henryk Kowalczyk</a></li><li><a href="http://wyborcza.pl/7,155287,22875507,jacek-sasin-nowym-szefem-komitetu-stalego-rady-ministrow-to.html" title="Jacek Sasin">Jacek Sasin</a></li><li><a href="http://wyborcza.pl/7,75410,22875881,paszporty-polityki-2017-tegoroczni-laureaci-jagoda-szelc.html" title="Paszporty Polityki 2017: laureaci">Paszporty Polityki 2017: laureaci</a></li><li><a href="http://bydgoszcz.wyborcza.pl/bydgoszcz/7,48722,22875851,szarza-z-widelcem-zakonczona-kownacki-na-wylocie-z-rzadu.html" title="Bartosz Kownacki">Bartosz Kownacki</a></li><li><a href="http://wyborcza.pl/7,75400,22873943,wajrak-minister-jan-szyszko-przejdzie-do-historii-jako-wyjatkowy.html?disableRedirects=true" title="Jan Szyszko">Jan Szyszko</a></li><li><a href="http://wyborcza.pl/7,156282,22874046,totalne-zaskoczenie-w-resorcie-kazdy-chwycil-za-telefon.html?disableRedirects=true" title="Ministerstwo cyfryzacji: Anna Streyska">Ministerstwo cyfryzacji: Anna Streyska</a></li><li><a href="http://wyborcza.pl/7,155287,22874886,nowy-minister-inwestycji-i-rozwoju-unijny-ekspert-jerzy-kwiecinski.html" title="Jerzy Kwieciski">Jerzy Kwieciski</a></li><li><a href="http://lodz.wyborcza.pl/lodz/7,35136,22875801,cezary-grabarczyk-zrezygnowalem-z-obrony-immunitetem.html" title="Cezary Grabarczyk">Cezary Grabarczyk</a></li><li><a href="http://wyborcza.pl/ksiazki/7,154165,22643551,wybralismy-10-ksiazek-roku-2017.html" title="Ksiki roku 2017">Ksiki roku 2017</a></li> +</ul> +</section><section> +<header> +<span>Tematy</span> +</header> +<ul> +<li><a href="http://wyborcza.pl/0,128956.html?tag=barbara+dziuk" title="Barbara Dziuk">Barbara Dziuk</a></li><li><a href="http://wyborcza.pl/0,128956.html?tag=Marek+P%EAk" title="Marek Pk">Marek Pk</a></li><li><a href="http://wyborcza.pl/0,128956.html?tag=Miros%B3aw+Pampuch" title="Mirosaw Pampuch">Mirosaw Pampuch</a></li><li><a href="http://wyborcza.pl/1,75398,19217826,posel-marek-opiola-o-nocnym-odwolaniu-szefow-sluzb-poslowie.html" title="Marek Opioa">Marek Opioa</a></li><li><a href="http://wyborcza.pl/magazyn/7,124059,22222760,czy-czeslaw-milosz-moze-byc-rzeczywiscie-zagrozeniem-dla.html" title="Czesaw Miosz">Czesaw Miosz</a></li><li><a href="http://wyborcza.pl/0,143644.html" title="Wybory samorzdowe 2018">Wybory samorzdowe 2018</a></li><li><a href="http://wyborcza.pl/56,140981,21698092,brigitte-macron.html" title="Brigitte Macron">Brigitte Macron</a></li><li><a href="http://wyborcza.pl/0,128956.html?tag=kleszcze" title="Kleszcze">Kleszcze</a></li><li><a href="http://wyborcza.pl/0,128956.html?tag=brexit+pytania" title="Brexit: pytania i odpowiedzi">Brexit: pytania i odpowiedzi</a></li><li><a href="http://wyborcza.pl/7,156282,21960784,od-dzisiaj-roaming-w-unii-europejskiej-przestal-obowiazywac.html" title="Roaming">Roaming</a></li><li><a href="http://warszawa.wyborcza.pl/warszawa/7,54420,21888478,rekrutacja-do-liceow-rekord-to-55-chetnych-na-miejsce.html" title="Rekrutacja do licew">Rekrutacja do licew</a></li><li><a href="http://wyborcza.biz/biznes/7,147743,21939448,abonament-rtv-wszystko-co-trzeba-wiedziec-w-kilku-prostych.html" title="Abonament RTV">Abonament RTV</a></li><li><a href="http://wyborcza.pl/7,75398,20946585,marks-spencer-zamyka-wszystkie-sklepy-w-polsce.html" title="Marks & Spencer">Marks & Spencer</a></li><li><a href="http://wyborcza.pl/alehistoria/1,121681,17844725,Ile_milionow_zginelo__Ofiary_II_wojny_swiatowej.html" title="Ofiary II wojny wiatowej">Ofiary II wojny wiatowej</a></li><li><a href="http://wyborcza.pl/1,97654,21220741,uzytkowanie-wieczyste-2017.html" title="Uytkowanie wieczyste 2017">Uytkowanie wieczyste 2017</a></li> +</ul> +</section><section> +<header> +<span>Informatory</span> +</header> +<ul> +<li><a href="http://wyborcza.pl/1,97654,21282743,zasilki-na-dzieci-w-2017-roku.html" title="Zasiek na dziecko 2017">Zasiek na dziecko 2017</a></li><li><a href="http://wyborcza.pl/7,97654,22392210,koniec-uzytkowania-wieczystego-informator.html" title="Uytkowanie wieczyste">Uytkowanie wieczyste</a></li><li><a href="http://wyborcza.biz/biznes/7,147880,21895075,waloryzacja-emerytur-w-2018-r-juz-wiemy-ile-wyniesie.html" title="Waloryzacja emerytur">Waloryzacja emerytur</a></li><li><a href="http://wyborcza.pl/7,75398,21968688,egzamin-gimnazjalny-2017-centralna-komisja-egzaminacyjna-podala.html" title="Egzamin gimnazjalny 2017: wyniki">Egzamin gimnazjalny 2017: wyniki</a></li><li><a href="http://wyborcza.pl/7,155287,21921609,placa-minimalna-w-2018-r-2080-zl-stawka-za-godzine-13-5.html" title="Paca minimalna 2018">Paca minimalna 2018</a></li><li><a href="http://wyborcza.pl/7,75398,20946585,marks-spencer-zamyka-wszystkie-sklepy-w-polsce.html" title="Marks & Spencer">Marks & Spencer</a></li><li><a href="http://wyborcza.pl/1,75398,20349918,jak-swiadek-koronny-masa-zostal-odtajniony-czy-doszlo-do.html" title="Masa odtajniony">Masa odtajniony</a></li><li><a href="http://wyborcza.pl/1,75399,20330335,egipt-jechac-czy-nie-jechac.html" title="Egipt: czy jecha?">Egipt: czy jecha?</a></li><li><a href="http://wyborcza.pl/1,75398,20046370,litewska-zbrodnia-majora-lupaszki.html" title="upaszka">upaszka</a></li><li><a href="http://wyborcza.pl/alehistoria/1,121681,17844725,Ile_milionow_zginelo__Ofiary_II_wojny_swiatowej.html" title="Ofiary II wojny wiatowej">Ofiary II wojny wiatowej</a></li><li><a href="http://wyborcza.pl/1,97654,21220741,uzytkowanie-wieczyste-2017.html" title="Uytkowanie wieczyste 2017">Uytkowanie wieczyste 2017</a></li><li><a href="http://wyborcza.pl/1,75248,15262278,Szybszy_internet_w_siedmiu_krokach__Co_zrobic__by.html" title="Szybszy internet">Szybszy internet</a></li><li><a href="http://wyborcza.pl/nekrologi/1,101500,18747235,kondolencje-podziekowania-wzory.html" title="Wzory: kondolencje - podzikowania">Wzory: kondolencje - podzikowania</a></li><li><a href="http://wyborcza.pl/1,97654,20849294,zmiany-w-emeryturach-z-krus.html" title="KRUS emerytura">KRUS emerytura</a></li><li><a href="http://wyborcza.pl/1,97654,20345805,komu-sie-nalezy-swiadczenie-przedemerytalne.html" title="wiadczenie przedemerytalne">wiadczenie przedemerytalne</a></li><li><a href="http://wyborcza.pl/1,97654,19802750,wczesniejsza-emerytura-nauczyciela.html" title="Wczeniejsza emerytura nauczyciela">Wczeniejsza emerytura nauczyciela</a></li><li><a href="http://wyborcza.pl/1,97654,19697904,jaki-podatek-od-nieruchomosci.html" title="Jaki podatek od nieruchomoci">Jaki podatek od nieruchomoci</a></li><li><a href="http://wyborcza.pl/1,97654,19533208,komu-sie-nalezy-renta-socjalna.html" title="Renta socjalna">Renta socjalna</a></li><li><a href="http://wyborcza.pl/1,97654,19344883,po-spadek-do-sadu-czy-do-notariusza.html" title="Spadek: sd czy notariusz">Spadek: sd czy notariusz</a></li><li><a href="http://wyborcza.pl/1,97654,19526974,komu-sie-nalezy-renta-rodzinna.html" title="Renta rodzinna">Renta rodzinna</a></li> +</ul> +</section><section> +<header> +<span>Nauka dla Kadego</span> +</header> +<ul> +<li><a href="http://wyborcza.pl/1,75248,6497648,Kleszcze__czyli_lesne_potwory.html" title="Kleszcze">Kleszcze</a></li><li><a href="http://wyborcza.pl/7,75400,21915146,polacy-rozszyfrowali-tajemnice-stwardnienia-rozsianego-mega-odkrycie.html" title="Stwardnienie rozsiane">Stwardnienie rozsiane</a></li><li><a href="http://wyborcza.pl/1,97654,19501961,przedawnienie-dlugow.html" title="Przedawnienie dugw">Przedawnienie dugw</a></li><li><a href="http://wyborcza.pl/1,97654,19139246,jak-czytac-wyniki-badan.html" title="Jak czyta wyniki bada">Jak czyta wyniki bada</a></li><li><a href="http://wyborcza.pl/1,97654,18726130,komu-sie-nalezy-dodatek-mieszkaniowy.html" title="Dodatek mieszkaniowy">Dodatek mieszkaniowy</a></li><li><a href="http://wyborcza.pl/1,97654,18083478,Jak_odzyskac_prawo_jazdy.html" title="Jak odzyska prawo jazdy">Jak odzyska prawo jazdy</a></li> +</ul> +</section><section> +<header> +<span>Tylko Zdrowie</span> +</header> +<ul> +<li><a href="http://wyborcza.pl/TylkoZdrowie/7,137474,22522336,ile-sie-zarabia-w-polskim-szpitalu-lista-plac.html" title="Szpital: Lista pac">Szpital: Lista pac</a></li><li><a href="http://wyborcza.pl/TylkoZdrowie/7,137474,22330718,rak-jelita-grubego-mozesz-mu-zapobiec-czy-badanie-jelita.html" title="Rak jelita grubego">Rak jelita grubego</a></li><li><a href="http://wyborcza.pl/TylkoZdrowie/7,137474,21879629,lody-weganskie-jak-je-zrobic.html" title="Lody wegaskie">Lody wegaskie</a></li><li><a href="http://wyborcza.pl/1,97654,17797883,Wazne_zmiany_w_kapitale_poczatkowym.html" title="Kapita pocztkowy zmiany">Kapita pocztkowy zmiany</a></li><li><a href="http://wyborcza.pl/1,97654,17787551,Emerytura_nauczyciela.html" title="Emerytura nauczyciela">Emerytura nauczyciela</a></li><li><a href="http://wyborcza.pl/1,97654,17414393,Za_ile_lat_emerytura_.html" title="Za ile lat emerytura?">Za ile lat emerytura?</a></li><li><a href="http://wyborcza.pl/1,97654,17264085,Spadek_i_darowizna_bez_placenia.html" title="Spadek i darowizna bez pacenia">Spadek i darowizna bez pacenia</a></li><li><a href="http://wyborcza.pl/1,97654,16322969,Testament_czy_darowizna__jak_przekazac_mieszkanie_.html" title="Testament czy darowizna">Testament czy darowizna</a></li><li><a href="http://wyborcza.pl/1,75398,19097048,235-miejsc-w-sejmie-dla-pis-pkw-podala-oficjalny-podzial-mandatow.html" title="Wyniki wyborw 2015 mandaty">Wyniki wyborw 2015 mandaty</a></li><li><a href="http://wyborcza.pl/7,75400,21726755,nadchodzi-wielki-atak-kleszczy.html" title="Kleszcze">Kleszcze</a></li><li><a href="http://wyborcza.pl/1,76842,7622197,Szumy_w_uszach__czyli_cisza__ktora_dzwoni_w_glowie.html" title="Szum w uszach">Szum w uszach</a></li> +</ul> +</section><section> +<header> +<span>Duy Format</span> +</header> +<ul> +<li><a href="http://wyborcza.pl/duzyformat/7,127290,22507549,angela-merkel-nas-zdradzila-dlaczego-6-milionow-niemcow.html" title="Angela Merkel">Angela Merkel</a></li><li><a href="http://wyborcza.pl/duzyformat/7,127290,22505059,policja-pod-willa-jaroslawa-kaczynskiego-policjanci-chronia.html" title="Jarosaw Kaczyski">Jarosaw Kaczyski</a></li><li><a href="http://wyborcza.pl/duzyformat/7,127290,21833158,jak-wstapilem-do-obrony-terytorialnej.html" title="Obrona Terytorialna">Obrona Terytorialna</a></li><li><a href="http://wyborcza.pl/1,76842,17920848,Fotelik_w_samochodzie___wazny_wzrost__a_nie_wiek_dziecka.html" title="Fotelik w samochodzie">Fotelik w samochodzie</a></li><li><a href="http://wyborcza.pl/1,97654,20039409,waloryzacja-emerytur-2017.html" title="Waloryzacja emerytur">Waloryzacja emerytur</a></li><li><a href="http://wyborcza.pl/TylkoZdrowie/1,137474,17101744,Dlaczego_masz_wzdety_brzuch.html" title="Wzdty brzuch">Wzdty brzuch</a></li> +</ul> +</section><section> +<header> +<span>Telewizyjna</span> +</header> +<ul> +<li><a href="http://wyborcza.pl/7,90535,20887556,wszystko-co-wiemy-o-3-sezonie-narcos-netflix-oglasza-obsade.html" title="Narcos">Narcos</a></li><li><a href="http://wyborcza.pl/TylkoZdrowie/1,137474,17101744,Dlaczego_masz_wzdety_brzuch.html" title="Wzdty brzuch">Wzdty brzuch</a></li> +</ul> +</section> +</div><!-- UZREditor --><!-- htmEOF --> +<!--17823192, [ /htm/17823/j17823192.htm ], null--> +
+
+ <div class="karaluch"> +<header> +<span>Tematy</span> +<i class="l"></i> +<i class="r"></i> +</header> +<section> +<header> +<span>Wiadomoci</span> +</header> +<ul> +<li><a href="http://wyborcza.biz/biznes/7,147768,24265091,wazne-zmiany-jak-w-nowym-roku-rozliczyc-sie-z-urzedem-skarbowym.html" title="PIT 2018">PIT 2018</a></li><li><a href="http://wyborcza.biz/biznes/0,100969.html" title="Depesze agencyjne">Depesze agencyjne</a></li><li><a href="http://wyborcza.pl/0,128956.html?tag=500+z%B3+na+dziecko" title="500 z na dziecko">500 z na dziecko</a></li><li><a href="http://wyborcza.pl/0,148081.html" title="Uchodcy">Uchodcy</a></li><li><a href="http://wyborcza.biz/Gieldy/1,114507,19778208,ceny-ropy-ostro-w-gore-w-kwietniu-wielki-naftowy-pakt-rosji.html" title="Cena ropy">Cena ropy</a></li><li><a href="http://wyborcza.pl/0,105742.html" title="Katastrofa smoleska">Katastrofa smoleska</a></li> +</ul> +</section><section> +<header> +<span>Gieda i Waluty</span> +</header> +<ul> +<li><a href="http://wyborcza.biz/Gieldy/0,114507.html" title="Wiadomoci giedowe">Wiadomoci giedowe</a></li><li><a href="http://wyborcza.biz/Gieldy/0,114514.html" title="Notowania GPW">Notowania GPW</a></li><li><a href="http://wyborcza.biz/Waluty/0,111138,8932151,,,Kursy_srednie_walut_NBP,A.html" title="Kursy walut NBP">Kursy walut NBP</a></li><li><a href="http://wyborcza.biz/Gieldy/0,125867.html" title="Surowce">Surowce</a></li><li><a href="http://wyborcza.biz/Gieldy/0,114544.html" title="Sownik giedowy">Sownik giedowy</a></li> +</ul> +</section><section> +<header> +<span>Firma</span> +</header> +<ul> +<li><a href="http://wyborcza.biz/biznes/0,147582.html" title="Wiadomoci">Wiadomoci</a></li> +</ul> +</section><section> +<header> +<span>Przetargi</span> +</header> +<ul> +<li><a href="http://www.komunikaty.pl/komunikaty/0,80849.html" title="Przetargi Wiadomoci">Przetargi Wiadomoci</a></li><li><a href="http://www.komunikaty.pl/komunikaty/0,79968.html" title="Przetargi Vademecum">Przetargi Vademecum</a></li><li><a href="http://www.komunikaty.pl/komunikaty/i/kategoria-Przetargi/1" title="Przetargi ogoszenia">Przetargi ogoszenia</a></li><li><a href="http://www.komunikaty.pl/komunikaty/i/kategoria-Przetargi/przedmiot-roboty+budowlane/1" title="Przetargi budowlane">Przetargi budowlane</a></li><li><a href="http://www.komunikaty.pl/komunikaty/i/kategoria-Nieruchomo%C5%9Bci/procedura-licytacja/1" title="Licytacje nieruchomoci">Licytacje nieruchomoci</a></li><li><a href="http://www.komunikaty.pl/komunikaty/i/kategoria-Ruchomo%C5%9Bci/procedura-licytacja/1" title="Licytacje ruchomoci">Licytacje ruchomoci</a></li> +</ul> +</section><section> +<header> +<span>Gieda i waluty</span> +</header> +<ul> +<li><a href="http://wyborcza.biz/Gieldy/0,116736,,,,NAME,A.html" title="Katalog spek">Katalog spek</a></li><li><a href="http://wyborcza.biz/Waluty/0,111026.html" title="Stopy procentowe">Stopy procentowe</a></li><li><a href="http://wyborcza.biz/Gieldy/0,114543.html" title="Analiza techniczna">Analiza techniczna</a></li> +</ul> +</section> +</div><!-- UZREditor --><!-- htmEOF --> +<!--17661432, [ /htm/17661/j17661432.htm ], null--> +
+
+
+
+
+
+
+
+
+
+
+
+
+ </div>
+ </div>
+
+ </div>
+
+ <div class="container-outer page-footer">
+ <div class="container-inner">
+ <div class="grid-row column-59" data-position="59,1-15">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<div class="container-outer page-footer">
+<div class="container-inner">
+<div class="grid-row">
+
+ <div id="footer2"> +<section class="foot-summary"> +<header class="foot-summary-head"> +<h2 class="foot-summary-head-title">Wyborcza.pl</h2> +<a href="http://wyborcza.pl" title="Wyborcza.pl"> +<img src="http://static.im-g.pl/i/obrazki/wyborcza2015/svg/new_wyborcza_pl.svg" alt="Wyborcza.pl"> +</a> +</header> +<div class="foot-summary-big"> +<input type="checkbox" id="foot-summary-big-services-show" class="foot-summary-big-services-show"> +<nav class="foot-summary-big-services"> +<div class="foot-summary-big-services-block"> +<p class="foot-summary-big-services-block-name"> +<label for="foot-summary-big-services-block-show-0" title="Wyborcza.pl">Wyborcza.pl</label> +</p> +<input type="checkbox" id="foot-summary-big-services-block-show-0" class="foot-summary-big-services-block-show"> +<ul class="foot-summary-big-services-block-list"> +<li><a href="http://wyborcza.pl/0,75398.html" title="Kraj" >Kraj</a></li><li><a href="http://wyborcza.pl/0,75399.html" title="wiat" >wiat</a></li><li><a href="http://wyborcza.pl/0,75968.html" title="Opinie" >Opinie</a></li><li><a href="http://wyborcza.pl/0,155287.html" title="Gospodarka" >Gospodarka</a></li><li><a href="http://wyborcza.pl/0,75400.html" title="Nauka" >Nauka</a></li><li><a href="http://wyborcza.pl/0,156282.html" title="Technologia" >Technologia</a></li><li><a href="http://wyborcza.pl/0,75410.html" title="Kultura" >Kultura</a></li><li><a href="http://wyborcza.pl/0,154903.html" title="Sport" >Sport</a></li><li><a href="http://wyborcza.pl/0,82983.html" title="Wideo" >Wideo</a></li><li><a href="http://wyborcza.pl/0,160910.html" title="The Wall Street Journal" >The Wall Street Journal</a></li><li><a href="http://wyborcza.pl/0,87647.html" title="Witamy w Polsce" >Witamy w Polsce</a></li><li><a href="http://wyborcza.pl/0,160795.html" title="Wyborcza Classic" >Wyborcza Classic</a></li> +</ul> +</div><div class="foot-summary-big-services-block"> +<p class="foot-summary-big-services-block-name"> +<label for="foot-summary-big-services-block-show-1" title="Wyborcza.biz">Wyborcza.biz</label> +</p> +<input type="checkbox" id="foot-summary-big-services-block-show-1" class="foot-summary-big-services-block-show"> +<ul class="foot-summary-big-services-block-list"> +<li><a href="http://wyborcza.biz/biznes/0,149543.html" title="Aktualnoci" >Aktualnoci</a></li><li><a href="http://wyborcza.biz/Gieldy/0,114514.html" title="Gieda" >Gieda</a></li><li><a href="http://wyborcza.biz/Waluty/0,111138,8932151,,,Kursy_srednie_walut_NBP,A.html" title="Wymiana walut" >Wymiana walut</a></li><li><a href="http://wyborcza.biz/biznes/0,147582.html" title="Zakupy i finanse" >Zakupy i finanse</a></li><li><a href="http://wyborcza.biz/biznes/0,147880.html" title="ZUS i emerytury" >ZUS i emerytury</a></li><li><a href="http://wyborcza.biz/biznes/0,147768.html" title="Podatki" >Podatki</a></li><li><a href="http://wyborcza.biz/biznes/0,159911.html" title="Praca" >Praca</a></li><li><a href="http://wyborcza.biz/biznes/0,156481.html#TRNavSST" title="Motoryzacja i podre" >Motoryzacja i podre</a></li><li><a href="http://wyborcza.biz/biznes/0,147758.html" title="Nieruchomoci" >Nieruchomoci</a></li> +</ul> +</div><div class="foot-summary-big-services-block"> +<p class="foot-summary-big-services-block-name"> +<label for="foot-summary-big-services-block-show-2" title="Serwisy lokalne">Serwisy lokalne</label> +</p> +<input type="checkbox" id="foot-summary-big-services-block-show-2" class="foot-summary-big-services-block-show"> +<ul class="foot-summary-big-services-block-list"> +<li><a href="http://bialystok.wyborcza.pl/bialystok/0,0.html" title="Biaystok" >Biaystok</a></li><li><a href="http://bielskobiala.wyborcza.pl/bielskobiala/0,0.html" title="Bielsko-Biaa" >Bielsko-Biaa</a></li><li><a href="http://bydgoszcz.wyborcza.pl/bydgoszcz/0,0.html" title="Bydgoszcz" >Bydgoszcz</a></li><li><a href="http://czestochowa.wyborcza.pl/czestochowa/0,0.html" title="Czstochowa" >Czstochowa</a></li><li><a href="http://gliwice.wyborcza.pl/gliwice/0,0.html" title="Gliwice" >Gliwice</a></li><li><a href="http://gorzow.wyborcza.pl/gorzow/0,0.html" title="Gorzw Wlkp." >Gorzw Wlkp.</a></li><li><a href="http://katowice.wyborcza.pl/katowice/0,0.html" title="Katowice" >Katowice</a></li><li><a href="http://kielce.wyborcza.pl/kielce/0,0.html" title="Kielce" >Kielce</a></li><li><a href="http://krakow.wyborcza.pl/krakow/0,0.html" title="Krakw" >Krakw</a></li><li><a href="http://lublin.wyborcza.pl/lublin/0,0.html" title="Lublin" >Lublin</a></li><li><a href="http://lodz.wyborcza.pl/lodz/0,0.html" title="d" >d</a></li><li><a href="http://olsztyn.wyborcza.pl/olsztyn/0,0.html" title="Olsztyn" >Olsztyn</a></li><li><a href="http://opole.wyborcza.pl/opole/0,0.html" title="Opole" >Opole</a></li><li><a href="http://plock.wyborcza.pl/plock/0,0.html" title="Pock" >Pock</a></li><li><a href="http://poznan.wyborcza.pl/poznan/0,0.html" title="Pozna" >Pozna</a></li><li><a href="http://radom.wyborcza.pl/radom/0,0.html" title="Radom" >Radom</a></li><li><a href="http://rzeszow.wyborcza.pl/rzeszow/0,0.html" title="Rzeszw" >Rzeszw</a></li><li><a href="http://sosnowiec.wyborcza.pl/sosnowiec/0,0.html" title="Sosnowiec" >Sosnowiec</a></li><li><a href="http://szczecin.wyborcza.pl/szczecin/0,0.html" title="Szczecin" >Szczecin</a></li><li><a href="http://torun.wyborcza.pl/torun/0,0.html" title="Toru" >Toru</a></li><li><a href="http://trojmiasto.wyborcza.pl/trojmiasto/0,0.html" title="Trjmiasto" >Trjmiasto</a></li><li><a href="http://warszawa.wyborcza.pl/warszawa/0,0.html" title="Warszawa" >Warszawa</a></li><li><a href="http://wroclaw.wyborcza.pl/wroclaw/0,0.html" title="Wrocaw" >Wrocaw</a></li><li><a href="http://zielonagora.wyborcza.pl/zielonagora/0,0.html" title="Zielona Gra" >Zielona Gra</a></li> +</ul> +</div><div class="foot-summary-big-services-block"> +<p class="foot-summary-big-services-block-name"> +<label for="foot-summary-big-services-block-show-3" title="Wysokieobcasy.pl">Wysokieobcasy.pl</label> +</p> +<input type="checkbox" id="foot-summary-big-services-block-show-3" class="foot-summary-big-services-block-show"> +<ul class="foot-summary-big-services-block-list"> +<li><a href="http://www.wysokieobcasy.pl/wysokie-obcasy/0,114757.html" title="Najnowsze" >Najnowsze</a></li><li><a href="http://www.wysokieobcasy.pl/wysokie-obcasy/0,100865.html" title="Gosy Kobiet" >Gosy Kobiet</a></li><li><a href="http://www.wysokieobcasy.pl/wysokie-obcasy/0,53664.html" title="Psychologia" >Psychologia</a></li><li><a href="http://www.wysokieobcasy.pl/wysokie-obcasy/0,66725.html" title="Wasze listy" >Wasze listy</a></li><li><a href="http://www.wysokieobcasy.pl/wysokie-obcasy/0,53662.html" title="Portrety Kobiet" >Portrety Kobiet</a></li><li><a href="http://www.wysokieobcasy.pl/wysokie-obcasy/0,53664.html" title="Psychologia" >Psychologia</a></li><li><a href="http://www.wysokieobcasy.pl/wysokie-obcasy/0,127763.html" title="Nowy Numer" >Nowy Numer</a></li><li><a href="http://www.wysokieobcasy.pl/wysokie-obcasy/0,152731.html" title="Wysokie Obcasy Extra" >Wysokie Obcasy Extra</a></li><li><a href="http://www.wysokieobcasy.pl/wysokie-obcasy/0,100961.html" title="Zdrowie i Uroda" >Zdrowie i Uroda</a></li><li><a href="http://www.wysokieobcasy.pl/wysokie-obcasy/0,53667.html" title="Jedzenie" >Jedzenie</a></li><li><a href="http://www.wysokieobcasy.pl/wysokie-obcasy/0,158669.html" title="IT Girls" >IT Girls</a></li> +</ul> +</div><div class="foot-summary-big-services-block"> +<p class="foot-summary-big-services-block-name"> +<label for="foot-summary-big-services-block-show-4" title="Magazyny">Magazyny</label> +</p> +<input type="checkbox" id="foot-summary-big-services-block-show-4" class="foot-summary-big-services-block-show"> +<ul class="foot-summary-big-services-block-list"> +<li><a href="http://wyborcza.pl/duzyformat/0,0.html#TRNavSST" title="Duy Format" >Duy Format</a></li><li><a href="http://wyborcza.pl/magazyn/0,0.html" title="Magazyn witeczny" >Magazyn witeczny</a></li><li><a href="http://wyborcza.pl/alehistoria/0,0.html" title="Ale Historia" >Ale Historia</a></li><li><a href="http://wyborcza.pl/TylkoZdrowie/0,0.html" title="Tylko zdrowie" >Tylko zdrowie</a></li><li><a href="http://wyborcza.pl/0,90535.html" title="Telewizyjna" >Telewizyjna</a></li><li><a href="http://wyborcza.pl/ksiazki/0,0.html" title="Ksiki" >Ksiki</a></li><li><a href="http://wyborcza.pl/osiemdziewiec/0,0.html" title="Osiem Dziewi" >Osiem Dziewi</a></li><li><a href="http://wyborcza.pl/0,79078.html" title="Poradniki" >Poradniki</a></li> +</ul> +</div> +</nav> +<div class="foot-summary-big-services-more"> +<label for="foot-summary-big-services-show">Wicej</label> +</div> +</div><div class="foot-summary-short"> +<ul class="foot-summary-short-links"> +<li><a href="http://biqdata.wyborcza.pl/" title="BIQdata.pl" >BIQdata.pl</a></li><li><a href="http://classic.wyborcza.pl/archiwumGW/0,0.html" title="Archiwum" >Archiwum</a></li><li><a href="http://www.komunikaty.pl" title="Komunikaty.pl" >Komunikaty.pl</a></li><li><a href="http://cojestgrane24.wyborcza.pl/cjg24/0,0.html" title="Cojestgrane24.pl" >Cojestgrane24.pl</a></li><li><a href="http://nekrologi.wyborcza.pl/0,0.html" title="Nekrologi" >Nekrologi</a></li> +</ul><div class="foot-summary-short-partners"> +<p class="foot-summary-short-partners-labels">Serwisy partnerskie</p> +<ul class="foot-summary-short-partners-list"> +<li><a href="http://gazeta.pl" title="Gazeta.pl" >Gazeta.pl</a></li><li><a href="http://www.tokfm.pl/Tokfm/0,0.html" title="TOK.fm" >TOK.fm</a></li><li><a href="http://sport.pl" title="Sport.pl" >Sport.pl</a></li><li><a href="http://publio.pl" title="Publio.pl" >Publio.pl</a></li><li><a href="http://kulturalnysklep.pl" title="Kulturalnysklep.pl" >Kulturalnysklep.pl</a></li> +</ul> +</div> +<a class="foot-summary-short-contact-link" href="http://wyborcza.pl/centrumpomocygw/0,134959.html" title="Napisz do redakcji"> +<figure> +<svg> +<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#letter"></use> +</svg> +</figure> +Napisz do redakcji +</a> +<div class="foot-summary-short-contact"> +<a class="foot-summary-short-contact-mail" href="mailto:redakcja@wyborcza.pl" title="Napisz do redakcji"> +<figure> +<svg> +<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#letter"></use> +</svg> +</figure> +Napisz do redakcji +</a> +</div> +</div><div class="foot-summary-buttons"> +<div class="foot-summary-buttons-subscription"> +<a class="foot-summary-buttons-subscription-link" title="Kup prenumerat" href="http://prenumerata.wyborcza.pl">Kup prenumerat</a> +</div> +<div class="foot-summary-buttons-stores"><a +href="https://itunes.apple.com/pl/app/gazeta-wyborcza/id530078918" +title="Aplikacja wyborcza.pl" +><svg> +<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#app-store"></use> +</svg></a><a +href="https://play.google.com/store/apps/details?id=pl.wyborcza.android.google&hl=pl" +title="Aplikacja wyborcza.pl" +><svg> +<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#google-play"></use> +</svg></a></div><!-- UZREditor --><!-- htmEOF -->
+
+
+
+
+ <ul class="foot-summary-buttons-share"> +<li class="foot-summary-buttons-share-face"> +<a href="https://www.facebook.com/wyborczabiznes" > +<figure> +<svg> +<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#face"></use> +</svg> +</figure> +</a> +</li><li class="foot-summary-buttons-share-tweet"> +<a href="https://twitter.com/wyborcza_biz" > +<figure> +<svg> +<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#tweet"></use> +</svg> +</figure> +</a> +</li> +<li class="foot-summary-buttons-share-newsletter" title="newsletter"> +<a href="http://wyborcza.pl/0,157210.html?fromnlt=stopka&refnlt=http://wyborcza.biz/">Newsletter</a> +</li> +</ul><!-- UZREditor --><!-- htmEOF -->
+ <!-- test stopki modul jest-->
+
+
+
+
+
+ </div>
+ <footer class="foot-menu">
+ <nav class="foot-menu-links">
+ <p class="foot-menu-links-copyright"><a href="http://www.agora.pl/" title="Agora SA">Copyright © Agora SA</a></p>
+ <ul>
+
+ <li><a href="http://wyborcza.pl/centrumpomocygw/0,134955.html" title="O nas" >O nas</a></li><li><a href="http://wyborcza.pl/centrumpomocygw/7,134956,23413995,polityka-prywatnosci.html" title="Prywatno" >Prywatno</a></li><li><a href="http://wyborcza.pl/centrumpomocygw/1,134957,8856780,Ogolne_zasady_udzielania_licencji_do_Materialow_Redakcyjnych.html" title="Licencje/Kontent" >Licencje/Kontent</a></li><li><a href="http://wyborcza.pl/reklamaGW/0,156164.html" title="Reklama w Internecie" >Reklama w Internecie</a></li><li><a href="http://wyborcza.pl/reklamaGW/0,104163.html" title="Reklama w papierze" >Reklama w papierze</a></li><li><a href="http://wyborcza.pl/centrumpomocygw/0,134959.html" title="Kontakt" >Kontakt</a></li><li><a href="http://wyborcza.pl/centrumpomocygw/0,134958.html" title="Zgo bd" rel="nofollow">Zgo bd</a></li><li><a href="http://wyborcza.pl/centrumpomocygw/0,134958.html" title="Pomoc" >Pomoc</a></li><!-- UZREditor --><!-- htmEOF -->
+
+ <!-- 21755068 -->
+ <li><a href="http://wyborcza.biz/biznes/3660000,0.html" title="Wszystkie artykuy">Wszystkie artykuy</a></li>
+ </ul>
+ </nav>
+ </footer>
+</div>
+</div>
+</div>
+</div>
+
+<!-- footerModule v1.0 --> +<!--22442678, [ /fix/modules/wyborcza/portal/footerModule.jsp ], wyborczaFooterModule--> +
+ </div>
+ </div>
+ </div>
+
+
+
+
+ + + + + +<!--13289483, [ /fix/modules/wyborcza/rodoAgreement.jsp ], rodoAgreementModule--> +
+
+
+
+
+
+ <script type='text/javascript'>
+ var _sf_async_config = _sf_async_config || {};
+
+ _sf_async_config.sections = '/biznes, kraj';
+ _sf_async_config.authors = 'Micha Frk';
+ var _cbq = window._cbq = (window._cbq || []);
+ _cbq.push(['_acct', 'anon']);
+
+ (function() {
+ function loadChartbeat() {
+ window._sf_endpt = (new Date()).getTime();
+ var e = document.createElement('script');
+ e.setAttribute('language', 'javascript');
+ e.setAttribute('type', 'text/javascript');
+ e.setAttribute('src', '//static.chartbeat.com/js/chartbeat_video.js');
+ document.body.appendChild(e);
+ }
+ var oldonload = window.onload;
+ window.onload = (typeof window.onload != 'function') ? loadChartbeat
+ : function() {
+ oldonload();
+ loadChartbeat();
+ };
+ })();
+ </script>
+
+
+
+<!-- chartbeatModule v1.3 -->
+
+
+
+
+
+
+<div style="display:none;">
+<img height="1" width="1" style="border-style:none;" alt="" src="http://www.googleadservices.com/pagead/conversion/1039774788/?label=avQqCOa77QEQxOjm7wM&guid=ON&script=0"/>
+</div>
+
+
+<!-- remarketingModule -->
+
+
+
+
+
+
+ <!-- pre-footer script -->
+ <script>!function(){function a(){k(),b(),l()}function b(){var a=document.querySelectorAll("img["+p+"]:not(."+m+"):not(."+n+")");[].forEach.call(a,function(a){c(a)})}function c(a){var b,c,f=e(a);i(a)&&f&&!h(a,f)&&(a.addClass(n),c=document.createElement("div"),c.addClass(o),j(c,a),b=new Image,b.src=f,b.onload=function(e){d(a,b,c)})}function d(a,b,c){a.setAttribute("src",b.src),a.addClass(m),a.removeClass(n),a.removeAttribute(p),a.removeAttribute(q),c.remove()}function e(a){var b=a.getAttribute(p)||!1,c=a.getAttribute(q)||!1;return f()&&!g()&&c&&(b=c),b}function f(){var a=!1;return"undefined"!=typeof gazeta_pl&&void 0!==gazeta_pl.mobileInfo&&gazeta_pl.mobileInfo.hasOwnProperty("isMobileDevice")&&gazeta_pl.mobileInfo.isMobileDevice&&(a=!0),a}function g(){var a=!1;return"undefined"!=typeof gazeta_pl&&void 0!==gazeta_pl.tabletInfo&&gazeta_pl.tabletInfo.hasOwnProperty("isTabletDevice")&&!0===gazeta_pl.tabletInfo.isTabletDevice&&(a=!0),a}function h(a,b){return a.src===b&&(a.complete&&0!==a.naturalHeight)}function i(a){var b=a.getBoundingClientRect(),c=document.documentElement.scrollTop,d=b.top+c,e=a.clientHeight,f=window.innerHeight,g=c-f,h=c+f+f,i=d+e;return d>g&&i<h}function j(a,b){b.parentNode.insertBefore(a,b.nextSibling)}function k(){"remove"in Element.prototype||(Element.prototype.remove=function(){this.parentNode&&this.parentNode.removeChild(this)}),Element.prototype.addClass=function(a){this.classList.add(a)},Element.prototype.removeClass=function(a){this.classList.remove(a)}}function l(){["scroll","resize"].map(function(a){window.addEventListener(a,b)})}var m="loaded",n="processing",o="preloaderContainer",p="data-src",q="data-src-mobile";a()}();</script>
+ <!-- /pre-footer script -->
+
+
+
+
+
+
+ <!-- wyborcza_common_skrypt not set -->
+
+
+ <script type="text/javascript" src="//static.im-g.pl/info/wyborcza-common/builds/17.4.108/main-min.jsgz"></script>
+
+
+
+
+
+
+
+
+ <!-- desktop script -->
+ <script type="text/javascript" src="//static.im-g.pl/wyborcza/wyborcza-biz/js/1.0.4/main-min.jsgz"></script>
+ <!-- /desktop script -->
+
+
+
+
+
+
+
+ <!-- footer script -->
+ <script></script>
+ <!-- /footer script -->
+
+
+
+<!-- scriptsModule v1.1 -->
+ + + + <img src="https://pubads.g.doubleclick.net/activity;dc_iu=/75224259/DFPAudiencePixel;ord=1;dc_seg=692406053?" width=1 height=1 border=0/> + +<!-- singleViewModule --> + + +
+
+
+
+
+<!-- deutscheWelleModule -->
+
+
+
+
+
+
+
+<!-- gemiusEfekt -->
+
+
+
+
+
+
+
+<!-- plistaModule-->
+
+
+ +<!--10185200, [ null ], aggregatorModule--> +
+
+
+
+<div>
+ <script id="adblock-modal" type="text/template"> +<div class="noadinfo"> +<div class="noadinfo-modal"> +<img src="https://bis.gazeta.pl/im/8/22327/m22327058.png" alt="" class="noadinfo-modal-image noadinfo-modal-image-desktop"/> +<img src="https://bis.gazeta.pl/im/7/22327/m22327057.png" alt="" class="noadinfo-modal-image noadinfo-modal-image-mobile"/> +<div class="noadinfo-modal-subscription">Jeeli jednak nie chcesz wycza adblocka, <a href="http://wyborcza.pl/prenumerataadb">kup prenumerat.</a> Zdecydowao si na to ju 100 tysicy internautw.</div> +<div class="noadinfo-modal-instruction"> +<div class="noadinfo-modal-show noadinfo-chrome"> +<label class="noadinfo-modal-instruction-tab noadinfo-modal-instruction-tab-active noadinfo-modal-instruction-tab-ab" for="option1">Mam Adblock</label> +<label class="noadinfo-modal-instruction-tab noadinfo-modal-instruction-tab-ap" for="option2">Mam Adblock Plus</label> +<div class="noadinfo-modal-instruction-holder"> +<input class="noadinfo-modal-input" type="radio" name="input-chrome" id="option1" checked /> +<div class="noadinfo-modal-detail"> +<p><b>Krok 1.</b> Kliknij w ikonk Adblocka obok pola do adresu oraz wybierz opcj: „Wstrzymaj blokowanie na stronach w tej domenie”.</p> +<img src="http://bi.gazeta.pl/im/4/22139/m22139974,CHROME-AB1.png" title="Instrukcja"> +<p><b>Krok 2.</b> Kliknij „Wyklucz”.</p> +<img src="http://bi.gazeta.pl/im/4/22156/m22156554,CHROME-AB2.png" title="Instrukcja"> +<p>Strona sama si odwiey i dostaniesz darmowe artykuy.</p> +</div> +<input class="noadinfo-modal-input" type="radio" name="input-chrome" id="option2" /> +<div class="noadinfo-modal-detail"> +<p><b>Krok 1.</b> Kliknij w ikonk Adblocka Plus obok pola do adresu oraz kliknij „Wczony na tej stronie”.</p> +<img src="http://bi.gazeta.pl/im/1/22139/m22139971,CHROME-AP1.png" title="Instrukcja"> +<p><b>Krok 2.</b> <a href="#" class="noadinfo-modal-instruction-reload" title="Odwie stron">Odwie stron</a>, aby dosta darmowe artykuy!</p> +</div> +</div> +</div> +<div class="noadinfo-modal-show noadinfo-firefox"> +<label class="noadinfo-modal-instruction-tab noadinfo-modal-instruction-tab-active noadinfo-modal-instruction-tab-ap" for="option3">Mam Adblock</label> +<label class="noadinfo-modal-instruction-tab noadinfo-modal-instruction-tab-ub" for="option4">Mam Ublock Origin</label> +<div class="noadinfo-modal-instruction-holder"> +<input class="noadinfo-modal-input" type="radio" name="input-firefox" id="option3" checked /> +<div class="noadinfo-modal-detail"> +<p><b>Krok 1.</b> Kliknij w ikonk Adblocka obok pola do adresu oraz kliknij: "Wycz blokowanie tylko na tej stronie".</p> +<img src="http://bi.gazeta.pl/im/2/22139/m22139972,FIREFOX-AP1.png" title="Instrukcja"> +<p><b>Krok 2.</b> <a href="#" class="noadinfo-modal-instruction-reload" title="Odwie stron">Odwie stron</a>, aby dosta darmowe artykuy!</p> +</div> +<input class="noadinfo-modal-input" type="radio" name="input-firefox" id="option4" /> +<div class="noadinfo-modal-detail"> +<p><b>Krok 1.</b> Kliknij w ikonk uBlock Origin obok pola do adresu oraz kliknij du niebiesk ikonk.</p> +<img src="http://bi.gazeta.pl/im/3/22139/m22139973,FIREFOX-UB1.png" title="Instrukcja"> +<p><b>Krok 2.</b> <a href="#" class="noadinfo-modal-instruction-reload" title="Odwie stron">Odwie stron</a>, aby dosta darmowe artykuy!</p> +</div> +</div> +</div> +</div> +</div> +</div> +</script><!-- UZREditor --><!--22060362,aliasOf--><!-- htmEOF -->
+</div>
+
+<!-- adBlockModule -->
+ +<!--12059452, [ /fix/modules/wyborcza/portal/adBlockModule.jsp ], emptyBean--> +
+
+
+ <script type="text/javascript" src="https://bis.gazeta.pl/info/mapa2.js"></script>
+ <!-- gemiusHeatMap -->
+
+ <div class="mcBan" id="mcBan_4"></div>
+ <div class="mcBan" id="mcBan_6"></div>
+
+
+ <div class="mcBan" id="glider1"></div>
+ <div class="mcBan" id="crawler1"></div>
+
+
+ + + + + +<!-- iSlayModule --> + +<!--14021450, [ /fix/modules/wyborcza/portal/iSlayModule.jsp ], iSlayModule--> +
+</body>
+</html>
diff --git a/test/fixtures/rich_media/oembed.html b/test/fixtures/rich_media/oembed.html new file mode 100644 index 000000000..55f17004b --- /dev/null +++ b/test/fixtures/rich_media/oembed.html @@ -0,0 +1,3 @@ +<link rel="alternate" type="application/json+oembed" + href="http://example.com/oembed.json" + title="Bacon Lollys oEmbed Profile" /> diff --git a/test/fixtures/rich_media/oembed.json b/test/fixtures/rich_media/oembed.json new file mode 100644 index 000000000..2a5f7a771 --- /dev/null +++ b/test/fixtures/rich_media/oembed.json @@ -0,0 +1 @@ +{"type":"photo","flickr_type":"photo","title":"Bacon Lollys","author_name":"\u202e\u202d\u202cbees\u202c","author_url":"https:\/\/www.flickr.com\/photos\/bees\/","width":"1024","height":"768","url":"https:\/\/farm4.staticflickr.com\/3040\/2362225867_4a87ab8baf_b.jpg","web_page":"https:\/\/www.flickr.com\/photos\/bees\/2362225867\/","thumbnail_url":"https:\/\/farm4.staticflickr.com\/3040\/2362225867_4a87ab8baf_q.jpg","thumbnail_width":150,"thumbnail_height":150,"web_page_short_url":"https:\/\/flic.kr\/p\/4AK2sc","license":"All Rights Reserved","license_id":0,"html":"<a data-flickr-embed=\"true\" href=\"https:\/\/www.flickr.com\/photos\/bees\/2362225867\/\" title=\"Bacon Lollys by \u202e\u202d\u202cbees\u202c, on Flickr\"><img src=\"https:\/\/farm4.staticflickr.com\/3040\/2362225867_4a87ab8baf_b.jpg\" width=\"1024\" height=\"768\" alt=\"Bacon Lollys\"><\/a><script async src=\"https:\/\/embedr.flickr.com\/assets\/client-code.js\" charset=\"utf-8\"><\/script>","version":"1.0","cache_age":3600,"provider_name":"Flickr","provider_url":"https:\/\/www.flickr.com\/"} diff --git a/test/fixtures/rich_media/ogp.html b/test/fixtures/rich_media/ogp.html new file mode 100644 index 000000000..c886b5871 --- /dev/null +++ b/test/fixtures/rich_media/ogp.html @@ -0,0 +1,9 @@ +<html prefix="og: http://ogp.me/ns#"> + <head> + <title>The Rock (1996)</title> + <meta property="og:title" content="The Rock" /> + <meta property="og:type" content="video.movie" /> + <meta property="og:url" content="http://www.imdb.com/title/tt0117500/" /> + <meta property="og:image" content="http://ia.media-imdb.com/images/rock.jpg" /> + </head> +</html> diff --git a/test/fixtures/rich_media/twitter_card.html b/test/fixtures/rich_media/twitter_card.html new file mode 100644 index 000000000..34c7c6ccd --- /dev/null +++ b/test/fixtures/rich_media/twitter_card.html @@ -0,0 +1,5 @@ +<meta name="twitter:card" content="summary" /> +<meta name="twitter:site" content="@flickr" /> +<meta name="twitter:title" content="Small Island Developing States Photo Submission" /> +<meta name="twitter:description" content="View the album on Flickr." /> +<meta name="twitter:image" content="https://farm6.staticflickr.com/5510/14338202952_93595258ff_z.jpg" /> diff --git a/test/flake_id_test.exs b/test/flake_id_test.exs new file mode 100644 index 000000000..ca2338041 --- /dev/null +++ b/test/flake_id_test.exs @@ -0,0 +1,42 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.FlakeIdTest do + use Pleroma.DataCase + import Kernel, except: [to_string: 1] + import Pleroma.FlakeId + + describe "fake flakes (compatibility with older serial integers)" do + test "from_string/1" do + fake_flake = <<0::integer-size(64), 42::integer-size(64)>> + assert from_string("42") == fake_flake + assert from_string(42) == fake_flake + end + + test "zero or -1 is a null flake" do + fake_flake = <<0::integer-size(128)>> + assert from_string("0") == fake_flake + assert from_string("-1") == fake_flake + end + + test "to_string/1" do + fake_flake = <<0::integer-size(64), 42::integer-size(64)>> + assert to_string(fake_flake) == "42" + end + end + + test "ecto type behaviour" do + flake = <<0, 0, 1, 104, 80, 229, 2, 235, 140, 22, 69, 201, 53, 210, 0, 0>> + flake_s = "9eoozpwTul5mjSEDRI" + + assert cast(flake) == {:ok, flake_s} + assert cast(flake_s) == {:ok, flake_s} + + assert load(flake) == {:ok, flake_s} + assert load(flake_s) == {:ok, flake_s} + + assert dump(flake_s) == {:ok, flake} + assert dump(flake) == {:ok, flake} + end +end diff --git a/test/formatter_test.exs b/test/formatter_test.exs index e4da84c10..e74985c4e 100644 --- a/test/formatter_test.exs +++ b/test/formatter_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.FormatterTest do alias Pleroma.Formatter alias Pleroma.User @@ -5,17 +9,28 @@ defmodule Pleroma.FormatterTest do import Pleroma.Factory + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + describe ".add_hashtag_links" do test "turns hashtags into links" do text = "I love #cofe and #2hu" expected_text = - "I love <a href='http://localhost:4001/tag/cofe' rel='tag'>#cofe</a> and <a href='http://localhost:4001/tag/2hu' rel='tag'>#2hu</a>" + "I love <a class='hashtag' data-tag='cofe' href='http://localhost:4001/tag/cofe' rel='tag'>#cofe</a> and <a class='hashtag' data-tag='2hu' href='http://localhost:4001/tag/2hu' rel='tag'>#2hu</a>" - tags = Formatter.parse_tags(text) + assert {^expected_text, [], _tags} = Formatter.linkify(text) + end - assert expected_text == - Formatter.add_hashtag_links({[], text}, tags) |> Formatter.finalize() + test "does not turn html characters to tags" do + text = "#fact_3: pleroma does what mastodon't" + + expected_text = + "<a class='hashtag' data-tag='fact_3' href='http://localhost:4001/tag/fact_3' rel='tag'>#fact_3</a>: pleroma does what mastodon't" + + assert {^expected_text, [], _tags} = Formatter.linkify(text) end end @@ -26,110 +41,109 @@ defmodule Pleroma.FormatterTest do expected = "Hey, check out <a href=\"https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla\">https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla</a> ." - assert Formatter.add_links({[], text}) |> Formatter.finalize() == expected + assert {^expected, [], []} = Formatter.linkify(text) text = "https://mastodon.social/@lambadalambda" expected = "<a href=\"https://mastodon.social/@lambadalambda\">https://mastodon.social/@lambadalambda</a>" - assert Formatter.add_links({[], text}) |> Formatter.finalize() == expected + assert {^expected, [], []} = Formatter.linkify(text) text = "https://mastodon.social:4000/@lambadalambda" expected = "<a href=\"https://mastodon.social:4000/@lambadalambda\">https://mastodon.social:4000/@lambadalambda</a>" - assert Formatter.add_links({[], text}) |> Formatter.finalize() == expected + assert {^expected, [], []} = Formatter.linkify(text) text = "@lambadalambda" expected = "@lambadalambda" - assert Formatter.add_links({[], text}) |> Formatter.finalize() == expected + assert {^expected, [], []} = Formatter.linkify(text) text = "http://www.cs.vu.nl/~ast/intel/" expected = "<a href=\"http://www.cs.vu.nl/~ast/intel/\">http://www.cs.vu.nl/~ast/intel/</a>" - assert Formatter.add_links({[], text}) |> Formatter.finalize() == expected + assert {^expected, [], []} = Formatter.linkify(text) text = "https://forum.zdoom.org/viewtopic.php?f=44&t=57087" expected = "<a href=\"https://forum.zdoom.org/viewtopic.php?f=44&t=57087\">https://forum.zdoom.org/viewtopic.php?f=44&t=57087</a>" - assert Formatter.add_links({[], text}) |> Formatter.finalize() == expected + assert {^expected, [], []} = Formatter.linkify(text) text = "https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul" expected = "<a href=\"https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul\">https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul</a>" - assert Formatter.add_links({[], text}) |> Formatter.finalize() == expected + assert {^expected, [], []} = Formatter.linkify(text) text = "https://www.google.co.jp/search?q=Nasim+Aghdam" expected = "<a href=\"https://www.google.co.jp/search?q=Nasim+Aghdam\">https://www.google.co.jp/search?q=Nasim+Aghdam</a>" - assert Formatter.add_links({[], text}) |> Formatter.finalize() == expected + assert {^expected, [], []} = Formatter.linkify(text) text = "https://en.wikipedia.org/wiki/Duff's_device" expected = "<a href=\"https://en.wikipedia.org/wiki/Duff's_device\">https://en.wikipedia.org/wiki/Duff's_device</a>" - assert Formatter.add_links({[], text}) |> Formatter.finalize() == expected + assert {^expected, [], []} = Formatter.linkify(text) text = "https://pleroma.com https://pleroma.com/sucks" expected = "<a href=\"https://pleroma.com\">https://pleroma.com</a> <a href=\"https://pleroma.com/sucks\">https://pleroma.com/sucks</a>" - assert Formatter.add_links({[], text}) |> Formatter.finalize() == expected + assert {^expected, [], []} = Formatter.linkify(text) text = "xmpp:contact@hacktivis.me" expected = "<a href=\"xmpp:contact@hacktivis.me\">xmpp:contact@hacktivis.me</a>" - assert Formatter.add_links({[], text}) |> Formatter.finalize() == expected + assert {^expected, [], []} = Formatter.linkify(text) text = "magnet:?xt=urn:btih:7ec9d298e91d6e4394d1379caf073c77ff3e3136&tr=udp%3A%2F%2Fopentor.org%3A2710&tr=udp%3A%2F%2Ftracker.blackunicorn.xyz%3A6969&tr=udp%3A%2F%2Ftracker.ccc.de%3A80&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com" expected = "<a href=\"#{text}\">#{text}</a>" - assert Formatter.add_links({[], text}) |> Formatter.finalize() == expected + assert {^expected, [], []} = Formatter.linkify(text) end end describe "add_user_links" do - test "gives a replacement for user links" do - text = "@gsimg According to @archaeme, that is @daggsy. Also hello @archaeme@archae.me" + test "gives a replacement for user links, using local nicknames in user links text" do + text = "@gsimg According to @archa_eme_, that is @daggsy. Also hello @archaeme@archae.me" gsimg = insert(:user, %{nickname: "gsimg"}) archaeme = insert(:user, %{ - nickname: "archaeme", - info: %Pleroma.User.Info{source_data: %{"url" => "https://archeme/@archaeme"}} + nickname: "archa_eme_", + info: %Pleroma.User.Info{source_data: %{"url" => "https://archeme/@archa_eme_"}} }) archaeme_remote = insert(:user, %{nickname: "archaeme@archae.me"}) - mentions = Pleroma.Formatter.parse_mentions(text) - - {subs, text} = Formatter.add_user_links({[], text}, mentions) + {text, mentions, []} = Formatter.linkify(text) - assert length(subs) == 3 - Enum.each(subs, fn {uuid, _} -> assert String.contains?(text, uuid) end) + assert length(mentions) == 3 expected_text = - "<span><a class='mention' href='#{gsimg.ap_id}'>@<span>gsimg</span></a></span> According to <span><a class='mention' href='#{ - "https://archeme/@archaeme" - }'>@<span>archaeme</span></a></span>, that is @daggsy. Also hello <span><a class='mention' href='#{ - archaeme_remote.ap_id - }'>@<span>archaeme</span></a></span>" - - assert expected_text == Formatter.finalize({subs, text}) + "<span class='h-card'><a data-user='#{gsimg.id}' class='u-url mention' href='#{ + gsimg.ap_id + }'>@<span>gsimg</span></a></span> According to <span class='h-card'><a data-user='#{ + archaeme.id + }' class='u-url mention' href='#{"https://archeme/@archa_eme_"}'>@<span>archa_eme_</span></a></span>, that is @daggsy. Also hello <span class='h-card'><a data-user='#{ + archaeme_remote.id + }' class='u-url mention' href='#{archaeme_remote.ap_id}'>@<span>archaeme</span></a></span>" + + assert expected_text == text end test "gives a replacement for user links when the user is using Osada" do @@ -137,46 +151,60 @@ defmodule Pleroma.FormatterTest do text = "@mike@osada.macgirvin.com test" - mentions = Formatter.parse_mentions(text) + {text, mentions, []} = Formatter.linkify(text) - {subs, text} = Formatter.add_user_links({[], text}, mentions) - - assert length(subs) == 1 - Enum.each(subs, fn {uuid, _} -> assert String.contains?(text, uuid) end) + assert length(mentions) == 1 expected_text = - "<span><a class='mention' href='#{mike.ap_id}'>@<span>mike</span></a></span> test" + "<span class='h-card'><a data-user='#{mike.id}' class='u-url mention' href='#{mike.ap_id}'>@<span>mike</span></a></span> test" - assert expected_text == Formatter.finalize({subs, text}) + assert expected_text == text end test "gives a replacement for single-character local nicknames" do text = "@o hi" o = insert(:user, %{nickname: "o"}) - mentions = Formatter.parse_mentions(text) + {text, mentions, []} = Formatter.linkify(text) - {subs, text} = Formatter.add_user_links({[], text}, mentions) + assert length(mentions) == 1 - assert length(subs) == 1 - Enum.each(subs, fn {uuid, _} -> assert String.contains?(text, uuid) end) + expected_text = + "<span class='h-card'><a data-user='#{o.id}' class='u-url mention' href='#{o.ap_id}'>@<span>o</span></a></span> hi" - expected_text = "<span><a class='mention' href='#{o.ap_id}'>@<span>o</span></a></span> hi" - assert expected_text == Formatter.finalize({subs, text}) + assert expected_text == text end test "does not give a replacement for single-character local nicknames who don't exist" do text = "@a hi" - mentions = Formatter.parse_mentions(text) + expected_text = "@a hi" + assert {^expected_text, [] = _mentions, [] = _tags} = Formatter.linkify(text) + end - {subs, text} = Formatter.add_user_links({[], text}, mentions) + test "given the 'safe_mention' option, it will only mention people in the beginning" do + user = insert(:user) + _other_user = insert(:user) + third_user = insert(:user) + text = " @#{user.nickname} hey dude i hate @#{third_user.nickname}" + {expected_text, mentions, [] = _tags} = Formatter.linkify(text, safe_mention: true) - assert length(subs) == 0 - Enum.each(subs, fn {uuid, _} -> assert String.contains?(text, uuid) end) + assert mentions == [{"@#{user.nickname}", user}] - expected_text = "@a hi" - assert expected_text == Formatter.finalize({subs, text}) + assert expected_text == + "<span class='h-card'><a data-user='#{user.id}' class='u-url mention' href='#{ + user.ap_id + }'>@<span>#{user.nickname}</span></a></span> hey dude i hate <span class='h-card'><a data-user='#{ + third_user.id + }' class='u-url mention' href='#{third_user.ap_id}'>@<span>#{third_user.nickname}</span></a></span>" + end + + test "given the 'safe_mention' option, it will still work without any mention" do + text = "A post without any mention" + {expected_text, mentions, [] = _tags} = Formatter.linkify(text, safe_mention: true) + + assert mentions == [] + assert expected_text == text end end @@ -184,31 +212,36 @@ defmodule Pleroma.FormatterTest do test "parses tags in the text" do text = "Here's a #Test. Maybe these are #working or not. What about #漢字? And #は。" - expected = [ + expected_tags = [ {"#Test", "test"}, {"#working", "working"}, - {"#漢字", "漢字"}, - {"#は", "は"} + {"#は", "は"}, + {"#漢字", "漢字"} ] - assert Formatter.parse_tags(text) == expected + assert {_text, [], ^expected_tags} = Formatter.linkify(text) end end test "it can parse mentions and return the relevant users" do - text = "@gsimg According to @archaeme, that is @daggsy. Also hello @archaeme@archae.me" + text = + "@@gsimg According to @archaeme, that is @daggsy. Also hello @archaeme@archae.me and @o and @@@jimm" + o = insert(:user, %{nickname: "o"}) + jimm = insert(:user, %{nickname: "jimm"}) gsimg = insert(:user, %{nickname: "gsimg"}) archaeme = insert(:user, %{nickname: "archaeme"}) archaeme_remote = insert(:user, %{nickname: "archaeme@archae.me"}) - expected_result = [ - {"@gsimg", gsimg}, + expected_mentions = [ {"@archaeme", archaeme}, - {"@archaeme@archae.me", archaeme_remote} + {"@archaeme@archae.me", archaeme_remote}, + {"@gsimg", gsimg}, + {"@jimm", jimm}, + {"@o", o} ] - assert Formatter.parse_mentions(text) == expected_result + assert {_text, ^expected_mentions, []} = Formatter.linkify(text) end test "it adds cool emoji" do @@ -238,7 +271,9 @@ defmodule Pleroma.FormatterTest do test "it returns the emoji used in the text" do text = "I love :moominmamma:" - assert Formatter.get_emoji(text) == [{"moominmamma", "/finmoji/128px/moominmamma-128.png"}] + assert Formatter.get_emoji(text) == [ + {"moominmamma", "/finmoji/128px/moominmamma-128.png", "Finmoji"} + ] end test "it returns a nice empty result when no emojis are present" do @@ -250,4 +285,11 @@ defmodule Pleroma.FormatterTest do text = nil assert Formatter.get_emoji(text) == [] end + + test "it escapes HTML in plain text" do + text = "hello & world google.com/?a=b&c=d \n http://test.com/?a=b&c=d 1" + expected = "hello & world google.com/?a=b&c=d \n http://test.com/?a=b&c=d 1" + + assert Formatter.html_escape(text, "text/plain") == expected + end end diff --git a/test/html_test.exs b/test/html_test.exs index f7150759b..0b5d3d892 100644 --- a/test/html_test.exs +++ b/test/html_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.HTMLTest do alias Pleroma.HTML use Pleroma.DataCase @@ -6,6 +10,8 @@ defmodule Pleroma.HTMLTest do <b>this is in bold</b> <p>this is a paragraph</p> this is a linebreak<br /> + this is a link with allowed "rel" attribute: <a href="http://example.com/" rel="tag">example.com</a> + this is a link with not allowed "rel" attribute: <a href="http://example.com/" rel="tag noallowed">example.com</a> this is an image: <img src="http://example.com/image.jpg"><br /> <script>alert('hacked')</script> """ @@ -20,6 +26,8 @@ defmodule Pleroma.HTMLTest do this is in bold this is a paragraph this is a linebreak + this is a link with allowed "rel" attribute: example.com + this is a link with not allowed "rel" attribute: example.com this is an image: alert('hacked') """ @@ -40,6 +48,8 @@ defmodule Pleroma.HTMLTest do this is in bold <p>this is a paragraph</p> this is a linebreak<br /> + this is a link with allowed "rel" attribute: <a href="http://example.com/" rel="tag">example.com</a> + this is a link with not allowed "rel" attribute: <a href="http://example.com/">example.com</a> this is an image: <img src="http://example.com/image.jpg" /><br /> alert('hacked') """ @@ -62,6 +72,8 @@ defmodule Pleroma.HTMLTest do <b>this is in bold</b> <p>this is a paragraph</p> this is a linebreak<br /> + this is a link with allowed "rel" attribute: <a href="http://example.com/" rel="tag">example.com</a> + this is a link with not allowed "rel" attribute: <a href="http://example.com/">example.com</a> this is an image: <img src="http://example.com/image.jpg" /><br /> alert('hacked') """ diff --git a/test/http_test.exs b/test/http_test.exs new file mode 100644 index 000000000..5f9522cf0 --- /dev/null +++ b/test/http_test.exs @@ -0,0 +1,59 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTTPTest do + use Pleroma.DataCase + import Tesla.Mock + + setup do + mock(fn + %{ + method: :get, + url: "http://example.com/hello", + headers: [{"content-type", "application/json"}] + } -> + json(%{"my" => "data"}) + + %{method: :get, url: "http://example.com/hello"} -> + %Tesla.Env{status: 200, body: "hello"} + + %{method: :post, url: "http://example.com/world"} -> + %Tesla.Env{status: 200, body: "world"} + end) + + :ok + end + + describe "get/1" do + test "returns successfully result" do + assert Pleroma.HTTP.get("http://example.com/hello") == { + :ok, + %Tesla.Env{status: 200, body: "hello"} + } + end + end + + describe "get/2 (with headers)" do + test "returns successfully result for json content-type" do + assert Pleroma.HTTP.get("http://example.com/hello", [{"content-type", "application/json"}]) == + { + :ok, + %Tesla.Env{ + status: 200, + body: "{\"my\":\"data\"}", + headers: [{"content-type", "application/json"}] + } + } + end + end + + describe "post/2" do + test "returns successfully result" do + assert Pleroma.HTTP.post("http://example.com/world", "") == { + :ok, + %Tesla.Env{status: 200, body: "world"} + } + end + end +end diff --git a/test/integration/mastodon_websocket_test.exs b/test/integration/mastodon_websocket_test.exs new file mode 100644 index 000000000..b42c9ef07 --- /dev/null +++ b/test/integration/mastodon_websocket_test.exs @@ -0,0 +1,101 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Integration.MastodonWebsocketTest do + use Pleroma.DataCase + + import Pleroma.Factory + + alias Pleroma.Integration.WebsocketClient + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.OAuth + alias Pleroma.Web.Streamer + + @path Pleroma.Web.Endpoint.url() + |> URI.parse() + |> Map.put(:scheme, "ws") + |> Map.put(:path, "/api/v1/streaming") + |> URI.to_string() + + setup do + GenServer.start(Streamer, %{}, name: Streamer) + + on_exit(fn -> + if pid = Process.whereis(Streamer) do + Process.exit(pid, :kill) + end + end) + end + + def start_socket(qs \\ nil, headers \\ []) do + path = + case qs do + nil -> @path + qs -> @path <> qs + end + + WebsocketClient.start_link(self(), path, headers) + end + + test "refuses invalid requests" do + assert {:error, {400, _}} = start_socket() + assert {:error, {404, _}} = start_socket("?stream=ncjdk") + end + + test "requires authentication and a valid token for protected streams" do + assert {:error, {403, _}} = start_socket("?stream=user&access_token=aaaaaaaaaaaa") + assert {:error, {403, _}} = start_socket("?stream=user") + end + + test "allows public streams without authentication" do + assert {:ok, _} = start_socket("?stream=public") + assert {:ok, _} = start_socket("?stream=public:local") + assert {:ok, _} = start_socket("?stream=hashtag&tag=lain") + end + + test "receives well formatted events" do + user = insert(:user) + {:ok, _} = start_socket("?stream=public") + {:ok, activity} = CommonAPI.post(user, %{"status" => "nice echo chamber"}) + + assert_receive {:text, raw_json}, 1_000 + assert {:ok, json} = Jason.decode(raw_json) + + assert "update" == json["event"] + assert json["payload"] + assert {:ok, json} = Jason.decode(json["payload"]) + + view_json = + Pleroma.Web.MastodonAPI.StatusView.render("status.json", activity: activity, for: nil) + |> Jason.encode!() + |> Jason.decode!() + + assert json == view_json + end + + describe "with a valid user token" do + setup do + {:ok, app} = + Pleroma.Repo.insert( + OAuth.App.register_changeset(%OAuth.App{}, %{ + client_name: "client", + scopes: ["scope"], + redirect_uris: "url" + }) + ) + + user = insert(:user) + + {:ok, auth} = OAuth.Authorization.create_authorization(app, user) + + {:ok, token} = OAuth.Token.exchange_token(app, auth) + + %{user: user, token: token} + end + + test "accepts valid tokens", state do + assert {:ok, _} = start_socket("?stream=user&access_token=#{state.token.token}") + end + end +end diff --git a/test/list_test.exs b/test/list_test.exs index 19eef8f6b..1909c0cd9 100644 --- a/test/list_test.exs +++ b/test/list_test.exs @@ -1,9 +1,12 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.ListTest do - alias Pleroma.{User, Repo} + alias Pleroma.Repo use Pleroma.DataCase import Pleroma.Factory - import Ecto.Query test "creating a list" do user = insert(:user) @@ -32,7 +35,7 @@ defmodule Pleroma.ListTest do user = insert(:user) other_user = insert(:user) {:ok, list} = Pleroma.List.create("title", user) - {:ok, %{following: following}} = Pleroma.List.follow(list, other_user) + {:ok, %{following: _following}} = Pleroma.List.follow(list, other_user) {:ok, %{following: following}} = Pleroma.List.unfollow(list, other_user) assert [] == following end diff --git a/test/media_proxy_test.exs b/test/media_proxy_test.exs index d71f9f13a..ddbadfbf5 100644 --- a/test/media_proxy_test.exs +++ b/test/media_proxy_test.exs @@ -1,6 +1,11 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.MediaProxyTest do use ExUnit.Case import Pleroma.Web.MediaProxy + alias Pleroma.Web.MediaProxy.MediaProxyController describe "when enabled" do setup do @@ -65,6 +70,14 @@ defmodule Pleroma.MediaProxyTest do assert decode_result(encoded) == url end + test "ensures urls are url-encoded" do + assert decode_result(url("https://pleroma.social/Hello world.jpg")) == + "https://pleroma.social/Hello%20world.jpg" + + assert decode_result(url("https://pleroma.social/Hello%20world.jpg")) == + "https://pleroma.social/Hello%20world.jpg" + end + test "validates signature" do secret_key_base = Pleroma.Config.get([Pleroma.Web.Endpoint, :secret_key_base]) @@ -83,6 +96,34 @@ defmodule Pleroma.MediaProxyTest do assert decode_url(sig, base64) == {:error, :invalid_signature} end + test "filename_matches matches url encoded paths" do + assert MediaProxyController.filename_matches( + true, + "/Hello%20world.jpg", + "http://pleroma.social/Hello world.jpg" + ) == :ok + + assert MediaProxyController.filename_matches( + true, + "/Hello%20world.jpg", + "http://pleroma.social/Hello%20world.jpg" + ) == :ok + end + + test "filename_matches matches non-url encoded paths" do + assert MediaProxyController.filename_matches( + true, + "/Hello world.jpg", + "http://pleroma.social/Hello%20world.jpg" + ) == :ok + + assert MediaProxyController.filename_matches( + true, + "/Hello world.jpg", + "http://pleroma.social/Hello world.jpg" + ) == :ok + end + test "uses the configured base_url" do base_url = Pleroma.Config.get([:media_proxy, :base_url]) @@ -99,6 +140,15 @@ defmodule Pleroma.MediaProxyTest do assert String.starts_with?(encoded, Pleroma.Config.get([:media_proxy, :base_url])) end + + # https://git.pleroma.social/pleroma/pleroma/issues/580 + test "encoding S3 links (must preserve `%2F`)" do + url = + "https://s3.amazonaws.com/example/test.png?X-Amz-Credential=your-access-key-id%2F20130721%2Fus-east-1%2Fs3%2Faws4_request" + + encoded = url(url) + assert decode_result(encoded) == url + end end describe "when disabled" do diff --git a/test/notification_test.exs b/test/notification_test.exs index a36ed5bb8..c3db77b6c 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -1,9 +1,14 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.NotificationTest do use Pleroma.DataCase - alias Pleroma.Web.TwitterAPI.TwitterAPI - alias Pleroma.Web.CommonAPI - alias Pleroma.{User, Notification} + alias Pleroma.Notification + alias Pleroma.User alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.TwitterAPI.TwitterAPI import Pleroma.Factory describe "create_notifications" do @@ -24,6 +29,18 @@ defmodule Pleroma.NotificationTest do assert notification.activity_id == activity.id assert other_notification.activity_id == activity.id end + + test "it creates a notification for subscribed users" do + user = insert(:user) + subscriber = insert(:user) + + User.subscribe(subscriber, user) + + {:ok, status} = TwitterAPI.create_status(user, %{"status" => "Akariiiin"}) + {:ok, [notification]} = Notification.create_notifications(status) + + assert notification.user_id == subscriber.id + end end describe "create_notification" do @@ -36,12 +53,140 @@ defmodule Pleroma.NotificationTest do assert nil == Notification.create_notification(activity, user) end + test "it doesn't create a notificatin for the user if the user mutes the activity author" do + muter = insert(:user) + muted = insert(:user) + {:ok, _} = User.mute(muter, muted) + muter = Repo.get(User, muter.id) + {:ok, activity} = CommonAPI.post(muted, %{"status" => "Hi @#{muter.nickname}"}) + + assert nil == Notification.create_notification(activity, muter) + end + + test "it doesn't create a notification for an activity from a muted thread" do + muter = insert(:user) + other_user = insert(:user) + {:ok, activity} = CommonAPI.post(muter, %{"status" => "hey"}) + CommonAPI.add_mute(muter, activity) + + {:ok, activity} = + CommonAPI.post(other_user, %{ + "status" => "Hi @#{muter.nickname}", + "in_reply_to_status_id" => activity.id + }) + + assert nil == Notification.create_notification(activity, muter) + end + + test "it disables notifications from people on remote instances" do + user = insert(:user, info: %{notification_settings: %{"remote" => false}}) + other_user = insert(:user) + + create_activity = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "actor" => other_user.ap_id, + "object" => %{ + "type" => "Note", + "content" => "Hi @#{user.nickname}", + "attributedTo" => other_user.ap_id + } + } + + {:ok, %{local: false} = activity} = Transmogrifier.handle_incoming(create_activity) + assert nil == Notification.create_notification(activity, user) + end + + test "it disables notifications from people on the local instance" do + user = insert(:user, info: %{notification_settings: %{"local" => false}}) + other_user = insert(:user) + {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hey @#{user.nickname}"}) + assert nil == Notification.create_notification(activity, user) + end + + test "it disables notifications from followers" do + follower = insert(:user) + followed = insert(:user, info: %{notification_settings: %{"followers" => false}}) + User.follow(follower, followed) + {:ok, activity} = CommonAPI.post(follower, %{"status" => "hey @#{followed.nickname}"}) + assert nil == Notification.create_notification(activity, followed) + end + + test "it disables notifications from people the user follows" do + follower = insert(:user, info: %{notification_settings: %{"follows" => false}}) + followed = insert(:user) + User.follow(follower, followed) + follower = Repo.get(User, follower.id) + {:ok, activity} = CommonAPI.post(followed, %{"status" => "hey @#{follower.nickname}"}) + assert nil == Notification.create_notification(activity, follower) + end + test "it doesn't create a notification for user if he is the activity author" do activity = insert(:note_activity) author = User.get_by_ap_id(activity.data["actor"]) assert nil == Notification.create_notification(activity, author) end + + test "it doesn't create a notification for follow-unfollow-follow chains" do + user = insert(:user) + followed_user = insert(:user) + {:ok, _, _, activity} = TwitterAPI.follow(user, %{"user_id" => followed_user.id}) + Notification.create_notification(activity, followed_user) + TwitterAPI.unfollow(user, %{"user_id" => followed_user.id}) + {:ok, _, _, activity_dupe} = TwitterAPI.follow(user, %{"user_id" => followed_user.id}) + assert nil == Notification.create_notification(activity_dupe, followed_user) + end + + test "it doesn't create a notification for like-unlike-like chains" do + user = insert(:user) + liked_user = insert(:user) + {:ok, status} = TwitterAPI.create_status(liked_user, %{"status" => "Yui is best yuru"}) + {:ok, fav_status} = TwitterAPI.fav(user, status.id) + Notification.create_notification(fav_status, liked_user) + TwitterAPI.unfav(user, status.id) + {:ok, dupe} = TwitterAPI.fav(user, status.id) + assert nil == Notification.create_notification(dupe, liked_user) + end + + test "it doesn't create a notification for repeat-unrepeat-repeat chains" do + user = insert(:user) + retweeted_user = insert(:user) + + {:ok, status} = + TwitterAPI.create_status(retweeted_user, %{ + "status" => "Send dupe notifications to the shadow realm" + }) + + {:ok, retweeted_activity} = TwitterAPI.repeat(user, status.id) + Notification.create_notification(retweeted_activity, retweeted_user) + TwitterAPI.unrepeat(user, status.id) + {:ok, dupe} = TwitterAPI.repeat(user, status.id) + assert nil == Notification.create_notification(dupe, retweeted_user) + end + + test "it doesn't create duplicate notifications for follow+subscribed users" do + user = insert(:user) + subscriber = insert(:user) + + {:ok, _, _, _} = TwitterAPI.follow(subscriber, %{"user_id" => user.id}) + User.subscribe(subscriber, user) + {:ok, status} = TwitterAPI.create_status(user, %{"status" => "Akariiiin"}) + {:ok, [_notif]} = Notification.create_notifications(status) + end + + test "it doesn't create subscription notifications if the recipient cannot see the status" do + user = insert(:user) + subscriber = insert(:user) + + User.subscribe(subscriber, user) + + {:ok, status} = + TwitterAPI.create_status(user, %{"status" => "inwisible", "visibility" => "direct"}) + + assert {:ok, []} == Notification.create_notifications(status) + end end describe "get notification" do @@ -127,12 +272,12 @@ defmodule Pleroma.NotificationTest do user = insert(:user) other_user = insert(:user) - {:ok, activity} = + {:ok, _activity} = TwitterAPI.create_status(user, %{ "status" => "hey @#{other_user.nickname}!" }) - {:ok, activity} = + {:ok, _activity} = TwitterAPI.create_status(user, %{ "status" => "hey again @#{other_user.nickname}!" }) @@ -142,14 +287,14 @@ defmodule Pleroma.NotificationTest do assert n2.id > n1.id - {:ok, activity} = + {:ok, _activity} = TwitterAPI.create_status(user, %{ "status" => "hey yet again @#{other_user.nickname}!" }) Notification.set_read_up_to(other_user, n2.id) - [n3, n2, n1] = notifs = Notification.for_user(other_user) + [n3, n2, n1] = Notification.for_user(other_user) assert n1.seen == true assert n2.seen == true @@ -258,7 +403,7 @@ defmodule Pleroma.NotificationTest do {:ok, activity} = CommonAPI.post(user, %{"status" => "test post"}) - assert length(Notification.for_user(user)) == 0 + assert Enum.empty?(Notification.for_user(user)) {:ok, _, _} = CommonAPI.favorite(activity.id, other_user) @@ -266,7 +411,7 @@ defmodule Pleroma.NotificationTest do {:ok, _} = CommonAPI.delete(activity.id, user) - assert length(Notification.for_user(user)) == 0 + assert Enum.empty?(Notification.for_user(user)) end test "liking an activity results in 1 notification, then 0 if the activity is unliked" do @@ -275,7 +420,7 @@ defmodule Pleroma.NotificationTest do {:ok, activity} = CommonAPI.post(user, %{"status" => "test post"}) - assert length(Notification.for_user(user)) == 0 + assert Enum.empty?(Notification.for_user(user)) {:ok, _, _} = CommonAPI.favorite(activity.id, other_user) @@ -283,7 +428,7 @@ defmodule Pleroma.NotificationTest do {:ok, _, _, _} = CommonAPI.unfavorite(activity.id, other_user) - assert length(Notification.for_user(user)) == 0 + assert Enum.empty?(Notification.for_user(user)) end test "repeating an activity results in 1 notification, then 0 if the activity is deleted" do @@ -292,7 +437,7 @@ defmodule Pleroma.NotificationTest do {:ok, activity} = CommonAPI.post(user, %{"status" => "test post"}) - assert length(Notification.for_user(user)) == 0 + assert Enum.empty?(Notification.for_user(user)) {:ok, _, _} = CommonAPI.repeat(activity.id, other_user) @@ -300,7 +445,7 @@ defmodule Pleroma.NotificationTest do {:ok, _} = CommonAPI.delete(activity.id, user) - assert length(Notification.for_user(user)) == 0 + assert Enum.empty?(Notification.for_user(user)) end test "repeating an activity results in 1 notification, then 0 if the activity is unrepeated" do @@ -309,7 +454,7 @@ defmodule Pleroma.NotificationTest do {:ok, activity} = CommonAPI.post(user, %{"status" => "test post"}) - assert length(Notification.for_user(user)) == 0 + assert Enum.empty?(Notification.for_user(user)) {:ok, _, _} = CommonAPI.repeat(activity.id, other_user) @@ -317,7 +462,7 @@ defmodule Pleroma.NotificationTest do {:ok, _, _} = CommonAPI.unrepeat(activity.id, other_user) - assert length(Notification.for_user(user)) == 0 + assert Enum.empty?(Notification.for_user(user)) end test "liking an activity which is already deleted does not generate a notification" do @@ -326,15 +471,15 @@ defmodule Pleroma.NotificationTest do {:ok, activity} = CommonAPI.post(user, %{"status" => "test post"}) - assert length(Notification.for_user(user)) == 0 + assert Enum.empty?(Notification.for_user(user)) {:ok, _deletion_activity} = CommonAPI.delete(activity.id, user) - assert length(Notification.for_user(user)) == 0 + assert Enum.empty?(Notification.for_user(user)) {:error, _} = CommonAPI.favorite(activity.id, other_user) - assert length(Notification.for_user(user)) == 0 + assert Enum.empty?(Notification.for_user(user)) end test "repeating an activity which is already deleted does not generate a notification" do @@ -343,15 +488,15 @@ defmodule Pleroma.NotificationTest do {:ok, activity} = CommonAPI.post(user, %{"status" => "test post"}) - assert length(Notification.for_user(user)) == 0 + assert Enum.empty?(Notification.for_user(user)) {:ok, _deletion_activity} = CommonAPI.delete(activity.id, user) - assert length(Notification.for_user(user)) == 0 + assert Enum.empty?(Notification.for_user(user)) {:error, _} = CommonAPI.repeat(activity.id, other_user) - assert length(Notification.for_user(user)) == 0 + assert Enum.empty?(Notification.for_user(user)) end test "replying to a deleted post without tagging does not generate a notification" do @@ -367,7 +512,7 @@ defmodule Pleroma.NotificationTest do "in_reply_to_status_id" => activity.id }) - assert length(Notification.for_user(user)) == 0 + assert Enum.empty?(Notification.for_user(user)) end end end diff --git a/test/object_test.exs b/test/object_test.exs index dac6c3be7..a30efd48c 100644 --- a/test/object_test.exs +++ b/test/object_test.exs @@ -1,7 +1,12 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.ObjectTest do use Pleroma.DataCase import Pleroma.Factory - alias Pleroma.{Repo, Object} + alias Pleroma.Object + alias Pleroma.Repo test "returns an object by it's AP id" do object = insert(:note) @@ -32,6 +37,8 @@ defmodule Pleroma.ObjectTest do found_object = Object.get_by_ap_id(object.data["id"]) refute object == found_object + + assert found_object.data["type"] == "Tombstone" end test "ensures cache is cleared for the object" do @@ -47,6 +54,8 @@ defmodule Pleroma.ObjectTest do cached_object = Object.get_cached_by_ap_id(object.data["id"]) refute object == cached_object + + assert cached_object.data["type"] == "Tombstone" end end diff --git a/test/plugs/admin_secret_authentication_plug_test.exs b/test/plugs/admin_secret_authentication_plug_test.exs new file mode 100644 index 000000000..e1d4b391f --- /dev/null +++ b/test/plugs/admin_secret_authentication_plug_test.exs @@ -0,0 +1,42 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.AdminSecretAuthenticationPlugTest do + use Pleroma.Web.ConnCase, async: true + import Pleroma.Factory + + alias Pleroma.Plugs.AdminSecretAuthenticationPlug + + test "does nothing if a user is assigned", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + + ret_conn = + conn + |> AdminSecretAuthenticationPlug.call(%{}) + + assert conn == ret_conn + end + + test "with secret set and given in the 'admin_token' parameter, it assigns an admin user", %{ + conn: conn + } do + Pleroma.Config.put(:admin_token, "password123") + + conn = + %{conn | params: %{"admin_token" => "wrong_password"}} + |> AdminSecretAuthenticationPlug.call(%{}) + + refute conn.assigns[:user] + + conn = + %{conn | params: %{"admin_token" => "password123"}} + |> AdminSecretAuthenticationPlug.call(%{}) + + assert conn.assigns[:user].info.is_admin + end +end diff --git a/test/plugs/authentication_plug_test.exs b/test/plugs/authentication_plug_test.exs index 061fa0cac..6158086ea 100644 --- a/test/plugs/authentication_plug_test.exs +++ b/test/plugs/authentication_plug_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Plugs.AuthenticationPlugTest do use Pleroma.Web.ConnCase, async: true diff --git a/test/plugs/basic_auth_decoder_plug_test.exs b/test/plugs/basic_auth_decoder_plug_test.exs index a4876fef7..4d7728e93 100644 --- a/test/plugs/basic_auth_decoder_plug_test.exs +++ b/test/plugs/basic_auth_decoder_plug_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Plugs.BasicAuthDecoderPlugTest do use Pleroma.Web.ConnCase, async: true diff --git a/test/plugs/ensure_authenticated_plug_test.exs b/test/plugs/ensure_authenticated_plug_test.exs index b32817fef..37ab5213a 100644 --- a/test/plugs/ensure_authenticated_plug_test.exs +++ b/test/plugs/ensure_authenticated_plug_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Plugs.EnsureAuthenticatedPlugTest do use Pleroma.Web.ConnCase, async: true diff --git a/test/plugs/ensure_user_key_plug_test.exs b/test/plugs/ensure_user_key_plug_test.exs index 9beda838e..6a9627f6a 100644 --- a/test/plugs/ensure_user_key_plug_test.exs +++ b/test/plugs/ensure_user_key_plug_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Plugs.EnsureUserKeyPlugTest do use Pleroma.Web.ConnCase, async: true diff --git a/test/plugs/http_security_plug_test.exs b/test/plugs/http_security_plug_test.exs index 169c3b3a8..0cbb7e4b1 100644 --- a/test/plugs/http_security_plug_test.exs +++ b/test/plugs/http_security_plug_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do use Pleroma.Web.ConnCase alias Pleroma.Config diff --git a/test/plugs/http_signature_plug_test.exs b/test/plugs/http_signature_plug_test.exs index a15c5b470..6a00dd4fd 100644 --- a/test/plugs/http_signature_plug_test.exs +++ b/test/plugs/http_signature_plug_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do use Pleroma.Web.ConnCase alias Pleroma.Web.HTTPSignatures diff --git a/test/plugs/instance_static_test.exs b/test/plugs/instance_static_test.exs new file mode 100644 index 000000000..e2dcfa3d8 --- /dev/null +++ b/test/plugs/instance_static_test.exs @@ -0,0 +1,47 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.RuntimeStaticPlugTest do + use Pleroma.Web.ConnCase + + @dir "test/tmp/instance_static" + + setup do + static_dir = Pleroma.Config.get([:instance, :static_dir]) + Pleroma.Config.put([:instance, :static_dir], @dir) + File.mkdir_p!(@dir) + + on_exit(fn -> + Pleroma.Config.put([:instance, :static_dir], static_dir) + File.rm_rf(@dir) + end) + end + + test "overrides index" do + bundled_index = get(build_conn(), "/") + assert html_response(bundled_index, 200) == File.read!("priv/static/index.html") + + File.write!(@dir <> "/index.html", "hello world") + + index = get(build_conn(), "/") + assert html_response(index, 200) == "hello world" + end + + test "overrides any file in static/static" do + bundled_index = get(build_conn(), "/static/terms-of-service.html") + + assert html_response(bundled_index, 200) == + File.read!("priv/static/static/terms-of-service.html") + + File.mkdir!(@dir <> "/static") + File.write!(@dir <> "/static/terms-of-service.html", "plz be kind") + + index = get(build_conn(), "/static/terms-of-service.html") + assert html_response(index, 200) == "plz be kind" + + File.write!(@dir <> "/static/kaniini.html", "<h1>rabbit hugs as a service</h1>") + index = get(build_conn(), "/static/kaniini.html") + assert html_response(index, 200) == "<h1>rabbit hugs as a service</h1>" + end +end diff --git a/test/plugs/legacy_authentication_plug_test.exs b/test/plugs/legacy_authentication_plug_test.exs index 383a22ff8..8b0b06772 100644 --- a/test/plugs/legacy_authentication_plug_test.exs +++ b/test/plugs/legacy_authentication_plug_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Plugs.LegacyAuthenticationPlugTest do use Pleroma.Web.ConnCase, async: true @@ -43,16 +47,18 @@ defmodule Pleroma.Plugs.LegacyAuthenticationPlugTest do |> assign(:auth_user, user) conn = - with_mock User, - reset_password: fn user, %{password: password, password_confirmation: password} -> - send(self(), :reset_password) - {:ok, user} - end do - conn - |> LegacyAuthenticationPlug.call(%{}) + with_mocks([ + {:crypt, [], [crypt: fn _password, password_hash -> password_hash end]}, + {User, [], + [ + reset_password: fn user, %{password: password, password_confirmation: password} -> + {:ok, user} + end + ]} + ]) do + LegacyAuthenticationPlug.call(conn, %{}) end - assert_received :reset_password assert conn.assigns.user == user end diff --git a/test/plugs/oauth_plug_test.exs b/test/plugs/oauth_plug_test.exs new file mode 100644 index 000000000..17fdba916 --- /dev/null +++ b/test/plugs/oauth_plug_test.exs @@ -0,0 +1,60 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.OAuthPlugTest do + use Pleroma.Web.ConnCase, async: true + + alias Pleroma.Plugs.OAuthPlug + import Pleroma.Factory + + @session_opts [ + store: :cookie, + key: "_test", + signing_salt: "cooldude" + ] + + setup %{conn: conn} do + user = insert(:user) + {:ok, %{token: token}} = Pleroma.Web.OAuth.Token.create_token(insert(:oauth_app), user) + %{user: user, token: token, conn: conn} + end + + test "with valid token(uppercase), it assigns the user", %{conn: conn} = opts do + conn = + conn + |> put_req_header("authorization", "BEARER #{opts[:token]}") + |> OAuthPlug.call(%{}) + + assert conn.assigns[:user] == opts[:user] + end + + test "with valid token(downcase), it assigns the user", %{conn: conn} = opts do + conn = + conn + |> put_req_header("authorization", "bearer #{opts[:token]}") + |> OAuthPlug.call(%{}) + + assert conn.assigns[:user] == opts[:user] + end + + test "with invalid token, it not assigns the user", %{conn: conn} do + conn = + conn + |> put_req_header("authorization", "bearer TTTTT") + |> OAuthPlug.call(%{}) + + refute conn.assigns[:user] + end + + test "when token is missed but token in session, it assigns the user", %{conn: conn} = opts do + conn = + conn + |> Plug.Session.call(Plug.Session.init(@session_opts)) + |> fetch_session() + |> put_session(:oauth_token, opts[:token]) + |> OAuthPlug.call(%{}) + + assert conn.assigns[:user] == opts[:user] + end +end diff --git a/test/plugs/oauth_scopes_plug_test.exs b/test/plugs/oauth_scopes_plug_test.exs new file mode 100644 index 000000000..f328026df --- /dev/null +++ b/test/plugs/oauth_scopes_plug_test.exs @@ -0,0 +1,122 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.OAuthScopesPlugTest do + use Pleroma.Web.ConnCase, async: true + + alias Pleroma.Plugs.OAuthScopesPlug + alias Pleroma.Repo + + import Pleroma.Factory + + test "proceeds with no op if `assigns[:token]` is nil", %{conn: conn} do + conn = + conn + |> assign(:user, insert(:user)) + |> OAuthScopesPlug.call(%{scopes: ["read"]}) + + refute conn.halted + assert conn.assigns[:user] + end + + test "proceeds with no op if `token.scopes` fulfill specified 'any of' conditions", %{ + conn: conn + } do + token = insert(:oauth_token, scopes: ["read", "write"]) |> Repo.preload(:user) + + conn = + conn + |> assign(:user, token.user) + |> assign(:token, token) + |> OAuthScopesPlug.call(%{scopes: ["read"]}) + + refute conn.halted + assert conn.assigns[:user] + end + + test "proceeds with no op if `token.scopes` fulfill specified 'all of' conditions", %{ + conn: conn + } do + token = insert(:oauth_token, scopes: ["scope1", "scope2", "scope3"]) |> Repo.preload(:user) + + conn = + conn + |> assign(:user, token.user) + |> assign(:token, token) + |> OAuthScopesPlug.call(%{scopes: ["scope2", "scope3"], op: :&}) + + refute conn.halted + assert conn.assigns[:user] + end + + test "proceeds with cleared `assigns[:user]` if `token.scopes` doesn't fulfill specified 'any of' conditions " <> + "and `fallback: :proceed_unauthenticated` option is specified", + %{conn: conn} do + token = insert(:oauth_token, scopes: ["read", "write"]) |> Repo.preload(:user) + + conn = + conn + |> assign(:user, token.user) + |> assign(:token, token) + |> OAuthScopesPlug.call(%{scopes: ["follow"], fallback: :proceed_unauthenticated}) + + refute conn.halted + refute conn.assigns[:user] + end + + test "proceeds with cleared `assigns[:user]` if `token.scopes` doesn't fulfill specified 'all of' conditions " <> + "and `fallback: :proceed_unauthenticated` option is specified", + %{conn: conn} do + token = insert(:oauth_token, scopes: ["read", "write"]) |> Repo.preload(:user) + + conn = + conn + |> assign(:user, token.user) + |> assign(:token, token) + |> OAuthScopesPlug.call(%{ + scopes: ["read", "follow"], + op: :&, + fallback: :proceed_unauthenticated + }) + + refute conn.halted + refute conn.assigns[:user] + end + + test "returns 403 and halts in case of no :fallback option and `token.scopes` not fulfilling specified 'any of' conditions", + %{conn: conn} do + token = insert(:oauth_token, scopes: ["read", "write"]) + any_of_scopes = ["follow"] + + conn = + conn + |> assign(:token, token) + |> OAuthScopesPlug.call(%{scopes: any_of_scopes}) + + assert conn.halted + assert 403 == conn.status + + expected_error = "Insufficient permissions: #{Enum.join(any_of_scopes, ", ")}." + assert Jason.encode!(%{error: expected_error}) == conn.resp_body + end + + test "returns 403 and halts in case of no :fallback option and `token.scopes` not fulfilling specified 'all of' conditions", + %{conn: conn} do + token = insert(:oauth_token, scopes: ["read", "write"]) + all_of_scopes = ["write", "follow"] + + conn = + conn + |> assign(:token, token) + |> OAuthScopesPlug.call(%{scopes: all_of_scopes, op: :&}) + + assert conn.halted + assert 403 == conn.status + + expected_error = + "Insufficient permissions: #{Enum.join(all_of_scopes -- token.scopes, ", ")}." + + assert Jason.encode!(%{error: expected_error}) == conn.resp_body + end +end diff --git a/test/plugs/session_authentication_plug_test.exs b/test/plugs/session_authentication_plug_test.exs index bb51bc0db..0000f4258 100644 --- a/test/plugs/session_authentication_plug_test.exs +++ b/test/plugs/session_authentication_plug_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Plugs.SessionAuthenticationPlugTest do use Pleroma.Web.ConnCase, async: true diff --git a/test/plugs/set_user_session_id_plug_test.exs b/test/plugs/set_user_session_id_plug_test.exs index 5edc0dab8..f8bfde039 100644 --- a/test/plugs/set_user_session_id_plug_test.exs +++ b/test/plugs/set_user_session_id_plug_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Plugs.SetUserSessionIdPlugTest do use Pleroma.Web.ConnCase, async: true @@ -28,6 +32,8 @@ defmodule Pleroma.Plugs.SetUserSessionIdPlugTest do end test "sets the user_id in the session to the user id of the user assign", %{conn: conn} do + Code.ensure_compiled(Pleroma.User) + conn = conn |> assign(:user, %User{id: 1}) diff --git a/test/plugs/uploaded_media_plug_test.exs b/test/plugs/uploaded_media_plug_test.exs new file mode 100644 index 000000000..49cf5396a --- /dev/null +++ b/test/plugs/uploaded_media_plug_test.exs @@ -0,0 +1,43 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.UploadedMediaPlugTest do + use Pleroma.Web.ConnCase + alias Pleroma.Upload + + defp upload_file(context) do + Pleroma.DataCase.ensure_local_uploader(context) + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: "nice_tf.jpg" + } + + {:ok, data} = Upload.store(file) + [%{"href" => attachment_url} | _] = data["url"] + [attachment_url: attachment_url] + end + + setup_all :upload_file + + test "does not send Content-Disposition header when name param is not set", %{ + attachment_url: attachment_url + } do + conn = get(build_conn(), attachment_url) + refute Enum.any?(conn.resp_headers, &(elem(&1, 0) == "content-disposition")) + end + + test "sends Content-Disposition header when name param is set", %{ + attachment_url: attachment_url + } do + conn = get(build_conn(), attachment_url <> "?name=\"cofe\".gif") + + assert Enum.any?( + conn.resp_headers, + &(&1 == {"content-disposition", "filename=\"\\\"cofe\\\".gif\""}) + ) + end +end diff --git a/test/plugs/user_enabled_plug_test.exs b/test/plugs/user_enabled_plug_test.exs index eeb167933..c0fafcab1 100644 --- a/test/plugs/user_enabled_plug_test.exs +++ b/test/plugs/user_enabled_plug_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Plugs.UserEnabledPlugTest do use Pleroma.Web.ConnCase, async: true diff --git a/test/plugs/user_fetcher_plug_test.exs b/test/plugs/user_fetcher_plug_test.exs index 5195a0c4a..262eb8d93 100644 --- a/test/plugs/user_fetcher_plug_test.exs +++ b/test/plugs/user_fetcher_plug_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Plugs.UserFetcherPlugTest do use Pleroma.Web.ConnCase, async: true diff --git a/test/plugs/user_is_admin_plug_test.exs b/test/plugs/user_is_admin_plug_test.exs index 031b2f466..9e05fff18 100644 --- a/test/plugs/user_is_admin_plug_test.exs +++ b/test/plugs/user_is_admin_plug_test.exs @@ -1,10 +1,14 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Plugs.UserIsAdminPlugTest do use Pleroma.Web.ConnCase, async: true alias Pleroma.Plugs.UserIsAdminPlug import Pleroma.Factory - test "accepts a user that is admin", %{conn: conn} do + test "accepts a user that is admin" do user = insert(:user, info: %{is_admin: true}) conn = @@ -18,7 +22,7 @@ defmodule Pleroma.Plugs.UserIsAdminPlugTest do assert conn == ret_conn end - test "denies a user that isn't admin", %{conn: conn} do + test "denies a user that isn't admin" do user = insert(:user) conn = @@ -29,7 +33,7 @@ defmodule Pleroma.Plugs.UserIsAdminPlugTest do assert conn.status == 403 end - test "denies when a user isn't set", %{conn: conn} do + test "denies when a user isn't set" do conn = build_conn() |> UserIsAdminPlug.call(%{}) diff --git a/test/registration_test.exs b/test/registration_test.exs new file mode 100644 index 000000000..6143b82c7 --- /dev/null +++ b/test/registration_test.exs @@ -0,0 +1,59 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.RegistrationTest do + use Pleroma.DataCase + + import Pleroma.Factory + + alias Pleroma.Registration + alias Pleroma.Repo + + describe "generic changeset" do + test "requires :provider, :uid" do + registration = build(:registration, provider: nil, uid: nil) + + cs = Registration.changeset(registration, %{}) + refute cs.valid? + + assert [ + provider: {"can't be blank", [validation: :required]}, + uid: {"can't be blank", [validation: :required]} + ] == cs.errors + end + + test "ensures uniqueness of [:provider, :uid]" do + registration = insert(:registration) + registration2 = build(:registration, provider: registration.provider, uid: registration.uid) + + cs = Registration.changeset(registration2, %{}) + assert cs.valid? + + assert {:error, + %Ecto.Changeset{ + errors: [ + uid: + {"has already been taken", + [constraint: :unique, constraint_name: "registrations_provider_uid_index"]} + ] + }} = Repo.insert(cs) + + # Note: multiple :uid values per [:user_id, :provider] are intentionally allowed + cs2 = Registration.changeset(registration2, %{uid: "available.uid"}) + assert cs2.valid? + assert {:ok, _} = Repo.insert(cs2) + + cs3 = Registration.changeset(registration2, %{provider: "provider2"}) + assert cs3.valid? + assert {:ok, _} = Repo.insert(cs3) + end + + test "allows `nil` :user_id (user-unbound registration)" do + registration = build(:registration, user_id: nil) + cs = Registration.changeset(registration, %{}) + assert cs.valid? + assert {:ok, _} = Repo.insert(cs) + end + end +end diff --git a/test/scheduled_activity_test.exs b/test/scheduled_activity_test.exs new file mode 100644 index 000000000..edc7cc3f9 --- /dev/null +++ b/test/scheduled_activity_test.exs @@ -0,0 +1,64 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ScheduledActivityTest do + use Pleroma.DataCase + alias Pleroma.DataCase + alias Pleroma.ScheduledActivity + import Pleroma.Factory + + setup context do + DataCase.ensure_local_uploader(context) + end + + describe "creation" do + test "when daily user limit is exceeded" do + user = insert(:user) + + today = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(6), :millisecond) + |> NaiveDateTime.to_iso8601() + + attrs = %{params: %{}, scheduled_at: today} + {:ok, _} = ScheduledActivity.create(user, attrs) + {:ok, _} = ScheduledActivity.create(user, attrs) + {:error, changeset} = ScheduledActivity.create(user, attrs) + assert changeset.errors == [scheduled_at: {"daily limit exceeded", []}] + end + + test "when total user limit is exceeded" do + user = insert(:user) + + today = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(6), :millisecond) + |> NaiveDateTime.to_iso8601() + + tomorrow = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.hours(36), :millisecond) + |> NaiveDateTime.to_iso8601() + + {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: today}) + {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: today}) + {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow}) + {:error, changeset} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow}) + assert changeset.errors == [scheduled_at: {"total limit exceeded", []}] + end + + test "when scheduled_at is earlier than 5 minute from now" do + user = insert(:user) + + scheduled_at = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(4), :millisecond) + |> NaiveDateTime.to_iso8601() + + attrs = %{params: %{}, scheduled_at: scheduled_at} + {:error, changeset} = ScheduledActivity.create(user, attrs) + assert changeset.errors == [scheduled_at: {"must be at least 5 minutes from now", []}] + end + end +end diff --git a/test/scheduled_activity_worker_test.exs b/test/scheduled_activity_worker_test.exs new file mode 100644 index 000000000..b9c91dda6 --- /dev/null +++ b/test/scheduled_activity_worker_test.exs @@ -0,0 +1,19 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ScheduledActivityWorkerTest do + use Pleroma.DataCase + alias Pleroma.ScheduledActivity + import Pleroma.Factory + + test "creates a status from the scheduled activity" do + user = insert(:user) + scheduled_activity = insert(:scheduled_activity, user: user, params: %{status: "hi"}) + Pleroma.ScheduledActivityWorker.perform(:execute, scheduled_activity.id) + + refute Repo.get(ScheduledActivity, scheduled_activity.id) + activity = Repo.all(Pleroma.Activity) |> Enum.find(&(&1.actor == user.ap_id)) + assert activity.data["object"]["content"] == "hi" + end +end diff --git a/test/support/builders/activity_builder.ex b/test/support/builders/activity_builder.ex index eb72d5ba9..6e5a8e059 100644 --- a/test/support/builders/activity_builder.ex +++ b/test/support/builders/activity_builder.ex @@ -1,5 +1,4 @@ defmodule Pleroma.Builders.ActivityBuilder do - alias Pleroma.Builders.UserBuilder alias Pleroma.Web.ActivityPub.ActivityPub def build(data \\ %{}, opts \\ %{}) do diff --git a/test/support/builders/user_builder.ex b/test/support/builders/user_builder.ex index 7a1ca79b5..f58e1b0ad 100644 --- a/test/support/builders/user_builder.ex +++ b/test/support/builders/user_builder.ex @@ -1,5 +1,6 @@ defmodule Pleroma.Builders.UserBuilder do - alias Pleroma.{User, Repo} + alias Pleroma.Repo + alias Pleroma.User def build(data \\ %{}) do user = %User{ diff --git a/test/support/captcha_mock.ex b/test/support/captcha_mock.ex new file mode 100644 index 000000000..ef4e68bc5 --- /dev/null +++ b/test/support/captcha_mock.ex @@ -0,0 +1,14 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Captcha.Mock do + alias Pleroma.Captcha.Service + @behaviour Service + + @impl Service + def new, do: %{type: :mock} + + @impl Service + def validate(_token, _captcha, _data), do: :ok +end diff --git a/test/support/channel_case.ex b/test/support/channel_case.ex index 68995a01d..466d8986f 100644 --- a/test/support/channel_case.ex +++ b/test/support/channel_case.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.ChannelCase do @moduledoc """ This module defines the test case to be used by diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 2e6707087..ec5892ff5 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.ConnCase do @moduledoc """ This module defines the test case to be used by @@ -19,6 +23,7 @@ defmodule Pleroma.Web.ConnCase do quote do # Import conveniences for testing with connections use Phoenix.ConnTest + use Pleroma.Tests.Helpers import Pleroma.Web.Router.Helpers # The default endpoint for testing @@ -28,6 +33,7 @@ defmodule Pleroma.Web.ConnCase do setup tags do Cachex.clear(:user_cache) + Cachex.clear(:object_cache) :ok = Ecto.Adapters.SQL.Sandbox.checkout(Pleroma.Repo) unless tags[:async] do diff --git a/test/support/data_case.ex b/test/support/data_case.ex index 8eff0fd94..df260bd3f 100644 --- a/test/support/data_case.ex +++ b/test/support/data_case.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.DataCase do @moduledoc """ This module defines the setup for tests requiring @@ -22,11 +26,13 @@ defmodule Pleroma.DataCase do import Ecto.Changeset import Ecto.Query import Pleroma.DataCase + use Pleroma.Tests.Helpers end end setup tags do Cachex.clear(:user_cache) + Cachex.clear(:object_cache) :ok = Ecto.Adapters.SQL.Sandbox.checkout(Pleroma.Repo) unless tags[:async] do @@ -36,6 +42,23 @@ defmodule Pleroma.DataCase do :ok end + def ensure_local_uploader(_context) do + uploader = Pleroma.Config.get([Pleroma.Upload, :uploader]) + filters = Pleroma.Config.get([Pleroma.Upload, :filters]) + + unless uploader == Pleroma.Uploaders.Local || filters != [] do + Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) + Pleroma.Config.put([Pleroma.Upload, :filters], []) + + on_exit(fn -> + Pleroma.Config.put([Pleroma.Upload, :uploader], uploader) + Pleroma.Config.put([Pleroma.Upload, :filters], filters) + end) + end + + :ok + end + @doc """ A helper that transform changeset errors to a map of messages. diff --git a/test/support/factory.ex b/test/support/factory.ex index 2889d8977..ea59912cf 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Factory do use ExMachina.Ecto, repo: Pleroma.Repo @@ -19,7 +23,7 @@ defmodule Pleroma.Factory do } end - def note_factory do + def note_factory(attrs \\ %{}) do text = sequence(:text, &"This is :moominmamma: note #{&1}") user = insert(:user) @@ -42,7 +46,7 @@ defmodule Pleroma.Factory do } %Pleroma.Object{ - data: data + data: merge_attributes(data, Map.get(attrs, :data, %{})) } end @@ -53,6 +57,24 @@ defmodule Pleroma.Factory do %Pleroma.Object{data: Map.merge(data, %{"to" => [user2.ap_id]})} end + def article_factory do + note_factory() + |> Map.put("type", "Article") + end + + def tombstone_factory do + data = %{ + "type" => "Tombstone", + "id" => Pleroma.Web.ActivityPub.Utils.generate_object_id(), + "formerType" => "Note", + "deleted" => DateTime.utc_now() |> DateTime.to_iso8601() + } + + %Pleroma.Object{ + data: data + } + end + def direct_note_activity_factory do dm = insert(:direct_note) @@ -73,8 +95,8 @@ defmodule Pleroma.Factory do } end - def note_activity_factory do - note = insert(:note) + def note_activity_factory(attrs \\ %{}) do + note = attrs[:note] || insert(:note) data = %{ "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(), @@ -93,9 +115,29 @@ defmodule Pleroma.Factory do } end - def announce_activity_factory do - note_activity = insert(:note_activity) - user = insert(:user) + def article_activity_factory do + article = insert(:article) + + data = %{ + "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(), + "type" => "Create", + "actor" => article.data["actor"], + "to" => article.data["to"], + "object" => article.data, + "published" => DateTime.utc_now() |> DateTime.to_iso8601(), + "context" => article.data["context"] + } + + %Pleroma.Activity{ + data: data, + actor: data["actor"], + recipients: data["to"] + } + end + + def announce_activity_factory(attrs \\ %{}) do + note_activity = attrs[:note_activity] || insert(:note_activity) + user = attrs[:user] || insert(:user) data = %{ "type" => "Announce", @@ -151,7 +193,7 @@ defmodule Pleroma.Factory do def websub_subscription_factory do %Pleroma.Web.Websub.WebsubServerSubscription{ topic: "http://example.org", - callback: "http://example/org/callback", + callback: "http://example.org/callback", secret: "here's a secret", valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 100), state: "requested" @@ -172,10 +214,81 @@ defmodule Pleroma.Factory do %Pleroma.Web.OAuth.App{ client_name: "Some client", redirect_uris: "https://example.com/callback", - scopes: "read", + scopes: ["read", "write", "follow", "push"], website: "https://example.com", - client_id: "aaabbb==", + client_id: Ecto.UUID.generate(), client_secret: "aaa;/&bbb" } end + + def instance_factory do + %Pleroma.Instances.Instance{ + host: "domain.com", + unreachable_since: nil + } + end + + def oauth_token_factory do + oauth_app = insert(:oauth_app) + + %Pleroma.Web.OAuth.Token{ + token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(), + refresh_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(), + user: build(:user), + app_id: oauth_app.id, + valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10) + } + end + + def oauth_authorization_factory do + %Pleroma.Web.OAuth.Authorization{ + token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false), + scopes: ["read", "write", "follow", "push"], + valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10), + user: build(:user), + app: build(:oauth_app) + } + end + + def push_subscription_factory do + %Pleroma.Web.Push.Subscription{ + user: build(:user), + token: build(:oauth_token), + endpoint: "https://example.com/example/1234", + key_auth: "8eDyX_uCN0XRhSbY5hs7Hg==", + key_p256dh: + "BCIWgsnyXDv1VkhqL2P7YRBvdeuDnlwAPT2guNhdIoW3IP7GmHh1SMKPLxRf7x8vJy6ZFK3ol2ohgn_-0yP7QQA=", + data: %{} + } + end + + def notification_factory do + %Pleroma.Notification{ + user: build(:user) + } + end + + def scheduled_activity_factory do + %Pleroma.ScheduledActivity{ + user: build(:user), + scheduled_at: NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(60), :millisecond), + params: build(:note) |> Map.from_struct() |> Map.get(:data) + } + end + + def registration_factory do + user = insert(:user) + + %Pleroma.Registration{ + user: user, + provider: "twitter", + uid: "171799000", + info: %{ + "name" => "John Doe", + "email" => "john@doe.com", + "nickname" => "johndoe", + "description" => "My bio" + } + } + end end diff --git a/test/support/helpers.ex b/test/support/helpers.ex new file mode 100644 index 000000000..6e389ce52 --- /dev/null +++ b/test/support/helpers.ex @@ -0,0 +1,29 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Tests.Helpers do + @moduledoc """ + Helpers for use in tests. + """ + + defmacro __using__(_opts) do + quote do + def refresh_record(%{id: id, __struct__: model} = _), + do: refresh_record(model, %{id: id}) + + def refresh_record(model, %{id: id} = _) do + Pleroma.Repo.get_by(model, id: id) + end + + # Used for comparing json rendering during tests. + def render_json(view, template, assigns) do + assigns = Map.new(assigns) + + view.render(template, assigns) + |> Poison.encode!() + |> Poison.decode!() + end + end + end +end diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex new file mode 100644 index 000000000..5b355bfe6 --- /dev/null +++ b/test/support/http_request_mock.ex @@ -0,0 +1,791 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule HttpRequestMock do + require Logger + + def request( + %Tesla.Env{ + url: url, + method: method, + headers: headers, + query: query, + body: body + } = _env + ) do + with {:ok, res} <- apply(__MODULE__, method, [url, query, body, headers]) do + res + else + {_, _r} = error -> + # Logger.warn(r) + error + end + end + + # GET Requests + # + def get(url, query \\ [], body \\ [], headers \\ []) + + def get("https://osada.macgirvin.com/channel/mike", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: + File.read!("test/fixtures/httpoison_mock/https___osada.macgirvin.com_channel_mike.json") + }} + end + + def get("https://mastodon.social/users/emelie/statuses/101849165031453009", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/status.emelie.json") + }} + end + + def get("https://mastodon.social/users/emelie", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/emelie.json") + }} + end + + def get( + "https://mastodon.social/.well-known/webfinger?resource=https://mastodon.social/users/emelie", + _, + _, + _ + ) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/webfinger_emelie.json") + }} + end + + def get("https://mastodon.social/users/emelie.atom", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/emelie.atom") + }} + end + + def get( + "https://osada.macgirvin.com/.well-known/webfinger?resource=acct:mike@osada.macgirvin.com", + _, + _, + Accept: "application/xrd+xml,application/jrd+json" + ) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/mike@osada.macgirvin.com.json") + }} + end + + def get( + "https://social.heldscal.la/.well-known/webfinger?resource=https://social.heldscal.la/user/29191", + _, + _, + Accept: "application/xrd+xml,application/jrd+json" + ) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/https___social.heldscal.la_user_29191.xml") + }} + end + + def get("https://pawoo.net/users/pekorino.atom", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/https___pawoo.net_users_pekorino.atom") + }} + end + + def get( + "https://pawoo.net/.well-known/webfinger?resource=acct:https://pawoo.net/users/pekorino", + _, + _, + Accept: "application/xrd+xml,application/jrd+json" + ) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/https___pawoo.net_users_pekorino.xml") + }} + end + + def get( + "https://social.stopwatchingus-heidelberg.de/api/statuses/user_timeline/18330.atom", + _, + _, + _ + ) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/atarifrosch_feed.xml") + }} + end + + def get( + "https://social.stopwatchingus-heidelberg.de/.well-known/webfinger?resource=acct:https://social.stopwatchingus-heidelberg.de/user/18330", + _, + _, + Accept: "application/xrd+xml,application/jrd+json" + ) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/atarifrosch_webfinger.xml") + }} + end + + def get("https://mamot.fr/users/Skruyb.atom", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/https___mamot.fr_users_Skruyb.atom") + }} + end + + def get( + "https://mamot.fr/.well-known/webfinger?resource=acct:https://mamot.fr/users/Skruyb", + _, + _, + Accept: "application/xrd+xml,application/jrd+json" + ) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/skruyb@mamot.fr.atom") + }} + end + + def get( + "https://social.heldscal.la/.well-known/webfinger?resource=nonexistant@social.heldscal.la", + _, + _, + Accept: "application/xrd+xml,application/jrd+json" + ) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/nonexistant@social.heldscal.la.xml") + }} + end + + def get( + "https://squeet.me/xrd/?uri=lain@squeet.me", + _, + _, + Accept: "application/xrd+xml,application/jrd+json" + ) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/lain_squeet.me_webfinger.xml") + }} + end + + def get( + "https://mst3k.interlinked.me/users/luciferMysticus", + _, + _, + Accept: "application/activity+json" + ) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/lucifermysticus.json") + }} + end + + def get("https://prismo.news/@mxb", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/https___prismo.news__mxb.json") + }} + end + + def get( + "https://hubzilla.example.org/channel/kaniini", + _, + _, + Accept: "application/activity+json" + ) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/kaniini@hubzilla.example.org.json") + }} + end + + def get("https://niu.moe/users/rye", _, _, Accept: "application/activity+json") do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/rye.json") + }} + end + + def get("http://mastodon.example.org/users/admin/statuses/100787282858396771", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: + File.read!( + "test/fixtures/httpoison_mock/http___mastodon.example.org_users_admin_status_1234.json" + ) + }} + end + + def get("https://puckipedia.com/", _, _, Accept: "application/activity+json") do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/puckipedia.com.json") + }} + end + + def get("https://peertube.moe/accounts/7even", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/7even.json") + }} + end + + def get("https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/peertube.moe-vid.json") + }} + end + + def get("https://baptiste.gelez.xyz/@/BaptisteGelez", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/baptiste.gelex.xyz-user.json") + }} + end + + def get("https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/baptiste.gelex.xyz-article.json") + }} + end + + def get("http://mastodon.example.org/users/admin", _, _, Accept: "application/activity+json") do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/admin@mastdon.example.org.json") + }} + end + + def get( + "http://mastodon.example.org/@admin/99541947525187367", + _, + _, + Accept: "application/activity+json" + ) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/mastodon-note-object.json") + }} + end + + def get("https://shitposter.club/notice/7369654", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/7369654.html") + }} + end + + def get("https://mstdn.io/users/mayuutann", _, _, Accept: "application/activity+json") do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/mayumayu.json") + }} + end + + def get( + "https://mstdn.io/users/mayuutann/statuses/99568293732299394", + _, + _, + Accept: "application/activity+json" + ) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/mayumayupost.json") + }} + end + + def get("https://pleroma.soykaf.com/users/lain/feed.atom", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: + File.read!( + "test/fixtures/httpoison_mock/https___pleroma.soykaf.com_users_lain_feed.atom.xml" + ) + }} + end + + def get(url, _, _, Accept: "application/xrd+xml,application/jrd+json") + when url in [ + "https://pleroma.soykaf.com/.well-known/webfinger?resource=acct:https://pleroma.soykaf.com/users/lain", + "https://pleroma.soykaf.com/.well-known/webfinger?resource=https://pleroma.soykaf.com/users/lain" + ] do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/https___pleroma.soykaf.com_users_lain.xml") + }} + end + + def get("https://shitposter.club/api/statuses/user_timeline/1.atom", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: + File.read!( + "test/fixtures/httpoison_mock/https___shitposter.club_api_statuses_user_timeline_1.atom.xml" + ) + }} + end + + def get( + "https://shitposter.club/.well-known/webfinger?resource=https://shitposter.club/user/1", + _, + _, + Accept: "application/xrd+xml,application/jrd+json" + ) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/https___shitposter.club_user_1.xml") + }} + end + + def get("https://shitposter.club/notice/2827873", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: + File.read!("test/fixtures/httpoison_mock/https___shitposter.club_notice_2827873.html") + }} + end + + def get("https://shitposter.club/api/statuses/show/2827873.atom", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: + File.read!( + "test/fixtures/httpoison_mock/https___shitposter.club_api_statuses_show_2827873.atom.xml" + ) + }} + end + + def get("https://testing.pleroma.lol/objects/b319022a-4946-44c5-9de9-34801f95507b", _, _, _) do + {:ok, %Tesla.Env{status: 200}} + end + + def get("https://shitposter.club/api/statuses/user_timeline/5381.atom", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/spc_5381.atom") + }} + end + + def get( + "https://shitposter.club/.well-known/webfinger?resource=https://shitposter.club/user/5381", + _, + _, + Accept: "application/xrd+xml,application/jrd+json" + ) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/spc_5381_xrd.xml") + }} + end + + def get("http://shitposter.club/.well-known/host-meta", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/shitposter.club_host_meta") + }} + end + + def get("https://shitposter.club/api/statuses/show/7369654.atom", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/7369654.atom") + }} + end + + def get("https://shitposter.club/notice/4027863", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/7369654.html") + }} + end + + def get("https://social.sakamoto.gq/users/eal/feed.atom", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/sakamoto_eal_feed.atom") + }} + end + + def get("http://social.sakamoto.gq/.well-known/host-meta", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/social.sakamoto.gq_host_meta") + }} + end + + def get( + "https://social.sakamoto.gq/.well-known/webfinger?resource=https://social.sakamoto.gq/users/eal", + _, + _, + Accept: "application/xrd+xml,application/jrd+json" + ) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/eal_sakamoto.xml") + }} + end + + def get( + "https://social.sakamoto.gq/objects/0ccc1a2c-66b0-4305-b23a-7f7f2b040056", + _, + _, + Accept: "application/atom+xml" + ) do + {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/httpoison_mock/sakamoto.atom")}} + end + + def get("http://mastodon.social/.well-known/host-meta", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/mastodon.social_host_meta") + }} + end + + def get( + "https://mastodon.social/.well-known/webfinger?resource=https://mastodon.social/users/lambadalambda", + _, + _, + Accept: "application/xrd+xml,application/jrd+json" + ) do + {:ok, + %Tesla.Env{ + status: 200, + body: + File.read!( + "test/fixtures/httpoison_mock/https___mastodon.social_users_lambadalambda.xml" + ) + }} + end + + def get("http://gs.example.org/.well-known/host-meta", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/gs.example.org_host_meta") + }} + end + + def get( + "http://gs.example.org/.well-known/webfinger?resource=http://gs.example.org:4040/index.php/user/1", + _, + _, + Accept: "application/xrd+xml,application/jrd+json" + ) do + {:ok, + %Tesla.Env{ + status: 200, + body: + File.read!( + "test/fixtures/httpoison_mock/http___gs.example.org_4040_index.php_user_1.xml" + ) + }} + end + + def get("http://gs.example.org/index.php/api/statuses/user_timeline/1.atom", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: + File.read!( + "test/fixtures/httpoison_mock/http__gs.example.org_index.php_api_statuses_user_timeline_1.atom.xml" + ) + }} + end + + def get("https://social.heldscal.la/api/statuses/user_timeline/29191.atom", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: + File.read!( + "test/fixtures/httpoison_mock/https___social.heldscal.la_api_statuses_user_timeline_29191.atom.xml" + ) + }} + end + + def get("http://squeet.me/.well-known/host-meta", _, _, _) do + {:ok, + %Tesla.Env{status: 200, body: File.read!("test/fixtures/httpoison_mock/squeet.me_host_meta")}} + end + + def get( + "https://squeet.me/xrd?uri=lain@squeet.me", + _, + _, + Accept: "application/xrd+xml,application/jrd+json" + ) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/lain_squeet.me_webfinger.xml") + }} + end + + def get( + "https://social.heldscal.la/.well-known/webfinger?resource=shp@social.heldscal.la", + _, + _, + Accept: "application/xrd+xml,application/jrd+json" + ) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/shp@social.heldscal.la.xml") + }} + end + + def get("http://framatube.org/.well-known/host-meta", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/framatube.org_host_meta") + }} + end + + def get( + "http://framatube.org/main/xrd?uri=framasoft@framatube.org", + _, + _, + Accept: "application/xrd+xml,application/jrd+json" + ) do + {:ok, + %Tesla.Env{ + status: 200, + headers: [{"content-type", "application/json"}], + body: File.read!("test/fixtures/httpoison_mock/framasoft@framatube.org.json") + }} + end + + def get("http://gnusocial.de/.well-known/host-meta", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/gnusocial.de_host_meta") + }} + end + + def get( + "http://gnusocial.de/main/xrd?uri=winterdienst@gnusocial.de", + _, + _, + Accept: "application/xrd+xml,application/jrd+json" + ) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/winterdienst_webfinger.json") + }} + end + + def get("http://status.alpicola.com/.well-known/host-meta", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/status.alpicola.com_host_meta") + }} + end + + def get("http://macgirvin.com/.well-known/host-meta", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/macgirvin.com_host_meta") + }} + end + + def get("http://gerzilla.de/.well-known/host-meta", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/gerzilla.de_host_meta") + }} + end + + def get( + "https://gerzilla.de/xrd/?uri=kaniini@gerzilla.de", + _, + _, + Accept: "application/xrd+xml,application/jrd+json" + ) do + {:ok, + %Tesla.Env{ + status: 200, + headers: [{"content-type", "application/json"}], + body: File.read!("test/fixtures/httpoison_mock/kaniini@gerzilla.de.json") + }} + end + + def get("https://social.heldscal.la/api/statuses/user_timeline/23211.atom", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: + File.read!( + "test/fixtures/httpoison_mock/https___social.heldscal.la_api_statuses_user_timeline_23211.atom.xml" + ) + }} + end + + def get( + "https://social.heldscal.la/.well-known/webfinger?resource=https://social.heldscal.la/user/23211", + _, + _, + _ + ) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/https___social.heldscal.la_user_23211.xml") + }} + end + + def get("http://social.heldscal.la/.well-known/host-meta", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/social.heldscal.la_host_meta") + }} + end + + def get("https://social.heldscal.la/.well-known/host-meta", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/social.heldscal.la_host_meta") + }} + end + + def get("https://mastodon.social/users/lambadalambda.atom", _, _, _) do + {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/lambadalambda.atom")}} + end + + def get("https://mastodon.social/users/lambadalambda", _, _, _) do + {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/lambadalambda.json")}} + end + + def get("https://social.heldscal.la/user/23211", _, _, Accept: "application/activity+json") do + {:ok, Tesla.Mock.json(%{"id" => "https://social.heldscal.la/user/23211"}, status: 200)} + end + + def get("http://example.com/ogp", _, _, _) do + {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/ogp.html")}} + end + + def get("http://example.com/malformed", _, _, _) do + {:ok, + %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/malformed-data.html")}} + end + + def get("http://example.com/empty", _, _, _) do + {:ok, %Tesla.Env{status: 200, body: "hello"}} + end + + def get("http://404.site" <> _, _, _, _) do + {:ok, + %Tesla.Env{ + status: 404, + body: "" + }} + end + + def get(url, query, body, headers) do + {:error, + "Not implemented the mock response for get #{inspect(url)}, #{query}, #{inspect(body)}, #{ + inspect(headers) + }"} + end + + # POST Requests + # + + def post(url, query \\ [], body \\ [], headers \\ []) + + def post("http://example.org/needs_refresh", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: "" + }} + end + + def post("http://200.site" <> _, _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: "" + }} + end + + def post("http://connrefused.site" <> _, _, _, _) do + {:error, :connrefused} + end + + def post("http://404.site" <> _, _, _, _) do + {:ok, + %Tesla.Env{ + status: 404, + body: "" + }} + end + + def post(url, _query, _body, _headers) do + {:error, "Not implemented the mock response for post #{inspect(url)}"} + end +end diff --git a/test/support/httpoison_mock.ex b/test/support/httpoison_mock.ex deleted file mode 100644 index e7344500f..000000000 --- a/test/support/httpoison_mock.ex +++ /dev/null @@ -1,883 +0,0 @@ -defmodule HTTPoisonMock do - alias HTTPoison.Response - - def process_request_options(options), do: options - - def get(url, body \\ [], headers \\ []) - - def get("https://prismo.news/@mxb", _, _) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/https___prismo.news__mxb.json") - }} - end - - def get("https://osada.macgirvin.com/channel/mike", _, _) do - {:ok, - %Response{ - status_code: 200, - body: - File.read!("test/fixtures/httpoison_mock/https___osada.macgirvin.com_channel_mike.json") - }} - end - - def get( - "https://osada.macgirvin.com/.well-known/webfinger?resource=acct:mike@osada.macgirvin.com", - _, - _ - ) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/mike@osada.macgirvin.com.json") - }} - end - - def get("https://info.pleroma.site/activity.json", _, _) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/https__info.pleroma.site_activity.json") - }} - end - - def get("https://info.pleroma.site/activity2.json", _, _) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/https__info.pleroma.site_activity2.json") - }} - end - - def get("https://info.pleroma.site/activity3.json", _, _) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/https__info.pleroma.site_activity3.json") - }} - end - - def get("https://info.pleroma.site/activity4.json", _, _) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/https__info.pleroma.site_activity4.json") - }} - end - - def get("https://info.pleroma.site/actor.json", _, _) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/https___info.pleroma.site_actor.json") - }} - end - - def get("https://puckipedia.com/", [Accept: "application/activity+json"], _) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/puckipedia.com.json") - }} - end - - def get( - "https://gerzilla.de/.well-known/webfinger?resource=acct:kaniini@gerzilla.de", - [Accept: "application/xrd+xml,application/jrd+json"], - follow_redirect: true - ) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/kaniini@gerzilla.de.json") - }} - end - - def get( - "https://framatube.org/.well-known/webfinger?resource=acct:framasoft@framatube.org", - [Accept: "application/xrd+xml,application/jrd+json"], - follow_redirect: true - ) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/framasoft@framatube.org.json") - }} - end - - def get( - "https://gnusocial.de/.well-known/webfinger?resource=acct:winterdienst@gnusocial.de", - [Accept: "application/xrd+xml,application/jrd+json"], - follow_redirect: true - ) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/winterdienst_webfinger.json") - }} - end - - def get( - "https://social.heldscal.la/.well-known/webfinger", - [Accept: "application/xrd+xml,application/jrd+json"], - params: [resource: "nonexistant@social.heldscal.la"], - follow_redirect: true - ) do - {:ok, - %Response{ - status_code: 500, - body: File.read!("test/fixtures/httpoison_mock/nonexistant@social.heldscal.la.xml") - }} - end - - def get( - "https://social.heldscal.la/.well-known/webfinger?resource=shp@social.heldscal.la", - [Accept: "application/xrd+xml,application/jrd+json"], - follow_redirect: true - ) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/shp@social.heldscal.la.xml") - }} - end - - def get( - "https://social.heldscal.la/.well-known/webfinger", - [Accept: "application/xrd+xml,application/jrd+json"], - params: [resource: "shp@social.heldscal.la"], - follow_redirect: true - ) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/shp@social.heldscal.la.xml") - }} - end - - def get( - "https://social.heldscal.la/.well-known/webfinger", - [Accept: "application/xrd+xml,application/jrd+json"], - params: [resource: "https://social.heldscal.la/user/23211"], - follow_redirect: true - ) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/https___social.heldscal.la_user_23211.xml") - }} - end - - def get( - "https://social.heldscal.la/.well-known/webfinger?resource=https://social.heldscal.la/user/23211", - [Accept: "application/xrd+xml,application/jrd+json"], - follow_redirect: true - ) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/https___social.heldscal.la_user_23211.xml") - }} - end - - def get( - "https://social.heldscal.la/.well-known/webfinger", - [Accept: "application/xrd+xml,application/jrd+json"], - params: [resource: "https://social.heldscal.la/user/29191"], - follow_redirect: true - ) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/https___social.heldscal.la_user_29191.xml") - }} - end - - def get( - "https://social.heldscal.la/.well-known/webfinger?resource=https://social.heldscal.la/user/29191", - [Accept: "application/xrd+xml,application/jrd+json"], - follow_redirect: true - ) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/https___social.heldscal.la_user_29191.xml") - }} - end - - def get( - "https://mastodon.social/.well-known/webfinger", - [Accept: "application/xrd+xml,application/jrd+json"], - params: [resource: "https://mastodon.social/users/lambadalambda"], - follow_redirect: true - ) do - {:ok, - %Response{ - status_code: 200, - body: - File.read!( - "test/fixtures/httpoison_mock/https___mastodon.social_users_lambadalambda.xml" - ) - }} - end - - def get( - "https://mastodon.social/.well-known/webfinger?resource=https://mastodon.social/users/lambadalambda", - [Accept: "application/xrd+xml,application/jrd+json"], - follow_redirect: true - ) do - {:ok, - %Response{ - status_code: 200, - body: - File.read!( - "test/fixtures/httpoison_mock/https___mastodon.social_users_lambadalambda.xml" - ) - }} - end - - def get( - "https://shitposter.club/.well-known/webfinger", - [Accept: "application/xrd+xml,application/jrd+json"], - params: [resource: "https://shitposter.club/user/1"], - follow_redirect: true - ) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/https___shitposter.club_user_1.xml") - }} - end - - def get( - "https://shitposter.club/.well-known/webfinger?resource=https://shitposter.club/user/1", - [Accept: "application/xrd+xml,application/jrd+json"], - follow_redirect: true - ) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/https___shitposter.club_user_1.xml") - }} - end - - def get( - "https://shitposter.club/.well-known/webfinger?resource=https://shitposter.club/user/5381", - [Accept: "application/xrd+xml,application/jrd+json"], - follow_redirect: true - ) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/spc_5381_xrd.xml") - }} - end - - def get( - "http://gs.example.org/.well-known/webfinger", - [Accept: "application/xrd+xml,application/jrd+json"], - params: [resource: "http://gs.example.org:4040/index.php/user/1"], - follow_redirect: true - ) do - {:ok, - %Response{ - status_code: 200, - body: - File.read!( - "test/fixtures/httpoison_mock/http___gs.example.org_4040_index.php_user_1.xml" - ) - }} - end - - def get( - "http://gs.example.org/.well-known/webfinger?resource=http://gs.example.org:4040/index.php/user/1", - [Accept: "application/xrd+xml,application/jrd+json"], - follow_redirect: true - ) do - {:ok, - %Response{ - status_code: 200, - body: - File.read!( - "test/fixtures/httpoison_mock/http___gs.example.org_4040_index.php_user_1.xml" - ) - }} - end - - def get( - "https://social.stopwatchingus-heidelberg.de/.well-known/webfinger?resource=https://social.stopwatchingus-heidelberg.de/user/18330", - [Accept: "application/xrd+xml,application/jrd+json"], - follow_redirect: true - ) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/atarifrosch_webfinger.xml") - }} - end - - def get( - "https://pleroma.soykaf.com/.well-known/webfinger", - [Accept: "application/xrd+xml,application/jrd+json"], - params: [resource: "https://pleroma.soykaf.com/users/lain"], - follow_redirect: true - ) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/https___pleroma.soykaf.com_users_lain.xml") - }} - end - - def get( - "https://pleroma.soykaf.com/.well-known/webfinger?resource=https://pleroma.soykaf.com/users/lain", - [Accept: "application/xrd+xml,application/jrd+json"], - follow_redirect: true - ) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/https___pleroma.soykaf.com_users_lain.xml") - }} - end - - def get("https://social.heldscal.la/api/statuses/user_timeline/29191.atom", _body, _headers) do - {:ok, - %Response{ - status_code: 200, - body: - File.read!( - "test/fixtures/httpoison_mock/https___social.heldscal.la_api_statuses_user_timeline_29191.atom.xml" - ) - }} - end - - def get("https://shitposter.club/api/statuses/user_timeline/5381.atom", _body, _headers) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/spc_5381.atom") - }} - end - - def get("https://social.heldscal.la/api/statuses/user_timeline/23211.atom", _body, _headers) do - {:ok, - %Response{ - status_code: 200, - body: - File.read!( - "test/fixtures/httpoison_mock/https___social.heldscal.la_api_statuses_user_timeline_23211.atom.xml" - ) - }} - end - - def get("https://mastodon.social/users/lambadalambda.atom", _body, _headers) do - {:ok, - %Response{ - status_code: 200, - body: - File.read!( - "test/fixtures/httpoison_mock/https___mastodon.social_users_lambadalambda.atom" - ) - }} - end - - def get( - "https://social.stopwatchingus-heidelberg.de/api/statuses/user_timeline/18330.atom", - _body, - _headers - ) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/atarifrosch_feed.xml") - }} - end - - def get("https://pleroma.soykaf.com/users/lain/feed.atom", _body, _headers) do - {:ok, - %Response{ - status_code: 200, - body: - File.read!( - "test/fixtures/httpoison_mock/https___pleroma.soykaf.com_users_lain_feed.atom.xml" - ) - }} - end - - def get("https://social.sakamoto.gq/users/eal/feed.atom", _body, _headers) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/sakamoto_eal_feed.atom") - }} - end - - def get("http://gs.example.org/index.php/api/statuses/user_timeline/1.atom", _body, _headers) do - {:ok, - %Response{ - status_code: 200, - body: - File.read!( - "test/fixtures/httpoison_mock/http__gs.example.org_index.php_api_statuses_user_timeline_1.atom.xml" - ) - }} - end - - def get("https://shitposter.club/notice/2827873", _body, _headers) do - {:ok, - %Response{ - status_code: 200, - body: - File.read!("test/fixtures/httpoison_mock/https___shitposter.club_notice_2827873.html") - }} - end - - def get("https://shitposter.club/api/statuses/show/2827873.atom", _body, _headers) do - {:ok, - %Response{ - status_code: 200, - body: - File.read!( - "test/fixtures/httpoison_mock/https___shitposter.club_api_statuses_show_2827873.atom.xml" - ) - }} - end - - def get("https://shitposter.club/api/statuses/user_timeline/1.atom", _body, _headers) do - {:ok, - %Response{ - status_code: 200, - body: - File.read!( - "test/fixtures/httpoison_mock/https___shitposter.club_api_statuses_user_timeline_1.atom.xml" - ) - }} - end - - def post( - "https://social.heldscal.la/main/push/hub", - {:form, _data}, - "Content-type": "application/x-www-form-urlencoded" - ) do - {:ok, - %Response{ - status_code: 202 - }} - end - - def get("http://mastodon.example.org/users/admin/statuses/100787282858396771", _, _) do - {:ok, - %Response{ - status_code: 200, - body: - File.read!( - "test/fixtures/httpoison_mock/http___mastodon.example.org_users_admin_status_1234.json" - ) - }} - end - - def get( - "https://pawoo.net/.well-known/webfinger", - [Accept: "application/xrd+xml,application/jrd+json"], - params: [resource: "https://pawoo.net/users/pekorino"], - follow_redirect: true - ) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/https___pawoo.net_users_pekorino.xml") - }} - end - - def get( - "https://pawoo.net/.well-known/webfinger?resource=https://pawoo.net/users/pekorino", - [Accept: "application/xrd+xml,application/jrd+json"], - follow_redirect: true - ) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/https___pawoo.net_users_pekorino.xml") - }} - end - - def get("https://pawoo.net/users/pekorino.atom", _, _) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/https___pawoo.net_users_pekorino.atom") - }} - end - - def get( - "https://mamot.fr/.well-known/webfinger", - [Accept: "application/xrd+xml,application/jrd+json"], - params: [resource: "https://mamot.fr/users/Skruyb"], - follow_redirect: true - ) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/skruyb@mamot.fr.atom") - }} - end - - def get( - "https://mamot.fr/.well-known/webfinger?resource=https://mamot.fr/users/Skruyb", - [Accept: "application/xrd+xml,application/jrd+json"], - follow_redirect: true - ) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/skruyb@mamot.fr.atom") - }} - end - - def get( - "https://social.sakamoto.gq/.well-known/webfinger", - [Accept: "application/xrd+xml,application/jrd+json"], - params: [resource: "https://social.sakamoto.gq/users/eal"], - follow_redirect: true - ) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/eal_sakamoto.xml") - }} - end - - def get( - "https://social.sakamoto.gq/.well-known/webfinger?resource=https://social.sakamoto.gq/users/eal", - [Accept: "application/xrd+xml,application/jrd+json"], - follow_redirect: true - ) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/eal_sakamoto.xml") - }} - end - - def get( - "https://pleroma.soykaf.com/.well-known/webfinger?resource=https://pleroma.soykaf.com/users/shp", - [Accept: "application/xrd+xml,application/jrd+json"], - follow_redirect: true - ) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/shp@pleroma.soykaf.com.webfigner") - }} - end - - def get( - "https://squeet.me/xrd/?uri=lain@squeet.me", - [Accept: "application/xrd+xml,application/jrd+json"], - follow_redirect: true - ) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/lain_squeet.me_webfinger.xml") - }} - end - - def get("https://mamot.fr/users/Skruyb.atom", _, _) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/https___mamot.fr_users_Skruyb.atom") - }} - end - - def get( - "https://social.sakamoto.gq/objects/0ccc1a2c-66b0-4305-b23a-7f7f2b040056", - [Accept: "application/atom+xml"], - _ - ) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/sakamoto.atom") - }} - end - - def get("https://pleroma.soykaf.com/users/shp/feed.atom", _, _) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/shp@pleroma.soykaf.com.feed") - }} - end - - def get("http://social.heldscal.la/.well-known/host-meta", [], follow_redirect: true) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/social.heldscal.la_host_meta") - }} - end - - def get("http://status.alpicola.com/.well-known/host-meta", [], follow_redirect: true) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/status.alpicola.com_host_meta") - }} - end - - def get("http://macgirvin.com/.well-known/host-meta", [], follow_redirect: true) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/macgirvin.com_host_meta") - }} - end - - def get("http://mastodon.social/.well-known/host-meta", [], follow_redirect: true) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/mastodon.social_host_meta") - }} - end - - def get("http://shitposter.club/.well-known/host-meta", [], follow_redirect: true) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/shitposter.club_host_meta") - }} - end - - def get("http://pleroma.soykaf.com/.well-known/host-meta", [], follow_redirect: true) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/pleroma.soykaf.com_host_meta") - }} - end - - def get("http://social.sakamoto.gq/.well-known/host-meta", [], follow_redirect: true) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/social.sakamoto.gq_host_meta") - }} - end - - def get("http://gs.example.org/.well-known/host-meta", [], follow_redirect: true) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/gs.example.org_host_meta") - }} - end - - def get("http://pawoo.net/.well-known/host-meta", [], follow_redirect: true) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/pawoo.net_host_meta") - }} - end - - def get("http://mamot.fr/.well-known/host-meta", [], follow_redirect: true) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/mamot.fr_host_meta") - }} - end - - def get("http://mastodon.xyz/.well-known/host-meta", [], follow_redirect: true) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/mastodon.xyz_host_meta") - }} - end - - def get("http://social.wxcafe.net/.well-known/host-meta", [], follow_redirect: true) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/social.wxcafe.net_host_meta") - }} - end - - def get("http://squeet.me/.well-known/host-meta", [], follow_redirect: true) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/squeet.me_host_meta") - }} - end - - def get( - "http://social.stopwatchingus-heidelberg.de/.well-known/host-meta", - [], - follow_redirect: true - ) do - {:ok, - %Response{ - status_code: 200, - body: - File.read!("test/fixtures/httpoison_mock/social.stopwatchingus-heidelberg.de_host_meta") - }} - end - - def get("http://mastodon.example.org/users/admin", [Accept: "application/activity+json"], _) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/admin@mastdon.example.org.json") - }} - end - - def get( - "https://hubzilla.example.org/channel/kaniini", - [Accept: "application/activity+json"], - _ - ) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/kaniini@hubzilla.example.org.json") - }} - end - - def get("https://masto.quad.moe/users/_HellPie", [Accept: "application/activity+json"], _) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/hellpie.json") - }} - end - - def get("https://niu.moe/users/rye", [Accept: "application/activity+json"], _) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/rye.json") - }} - end - - def get("https://n1u.moe/users/rye", [Accept: "application/activity+json"], _) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/rye.json") - }} - end - - def get( - "https://mst3k.interlinked.me/users/luciferMysticus", - [Accept: "application/activity+json"], - _ - ) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/lucifermysticus.json") - }} - end - - def get("https://mstdn.io/users/mayuutann", [Accept: "application/activity+json"], _) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/mayumayu.json") - }} - end - - def get( - "http://mastodon.example.org/@admin/99541947525187367", - [Accept: "application/activity+json"], - _ - ) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/mastodon-note-object.json") - }} - end - - def get( - "https://mstdn.io/users/mayuutann/statuses/99568293732299394", - [Accept: "application/activity+json"], - _ - ) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/mayumayupost.json") - }} - end - - def get("https://shitposter.club/notice/7369654", _, _) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/7369654.html") - }} - end - - def get("https://shitposter.club/api/statuses/show/7369654.atom", _body, _headers) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/7369654.atom") - }} - end - - def get("https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/", _, _) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/baptiste.gelex.xyz-article.json") - }} - end - - def get("https://baptiste.gelez.xyz/@/BaptisteGelez", _, _) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/baptiste.gelex.xyz-user.json") - }} - end - - def get("https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3", _, _) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/peertube.moe-vid.json") - }} - end - - def get("https://peertube.moe/accounts/7even", _, _) do - {:ok, - %Response{ - status_code: 200, - body: File.read!("test/fixtures/httpoison_mock/7even.json") - }} - end - - def get(url, body, headers) do - {:error, - "Not implemented the mock response for get #{inspect(url)}, #{inspect(body)}, #{ - inspect(headers) - }"} - end - - def post(url, _body, _headers) do - {:error, "Not implemented the mock response for post #{inspect(url)}"} - end - - def post(url, _body, _headers, _options) do - {:error, "Not implemented the mock response for post #{inspect(url)}"} - end -end diff --git a/test/support/ostatus_mock.ex b/test/support/ostatus_mock.ex index 36865ae02..9c0f2f323 100644 --- a/test/support/ostatus_mock.ex +++ b/test/support/ostatus_mock.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.OStatusMock do import Pleroma.Factory diff --git a/test/support/web_push_http_client_mock.ex b/test/support/web_push_http_client_mock.ex new file mode 100644 index 000000000..d8accd21c --- /dev/null +++ b/test/support/web_push_http_client_mock.ex @@ -0,0 +1,23 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.WebPushHttpClientMock do + def get(url, headers \\ [], options \\ []) do + { + res, + %Tesla.Env{status: status} + } = Pleroma.HTTP.request(:get, url, "", headers, options) + + {res, %{status_code: status}} + end + + def post(url, body, headers \\ [], options \\ []) do + { + res, + %Tesla.Env{status: status} + } = Pleroma.HTTP.request(:post, url, body, headers, options) + + {res, %{status_code: status}} + end +end diff --git a/test/support/websocket_client.ex b/test/support/websocket_client.ex new file mode 100644 index 000000000..121231452 --- /dev/null +++ b/test/support/websocket_client.ex @@ -0,0 +1,62 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Integration.WebsocketClient do + # https://github.com/phoenixframework/phoenix/blob/master/test/support/websocket_client.exs + + @doc """ + Starts the WebSocket server for given ws URL. Received Socket.Message's + are forwarded to the sender pid + """ + def start_link(sender, url, headers \\ []) do + :crypto.start() + :ssl.start() + + :websocket_client.start_link( + String.to_charlist(url), + __MODULE__, + [sender], + extra_headers: headers + ) + end + + @doc """ + Closes the socket + """ + def close(socket) do + send(socket, :close) + end + + @doc """ + Sends a low-level text message to the client. + """ + def send_text(server_pid, msg) do + send(server_pid, {:text, msg}) + end + + @doc false + def init([sender], _conn_state) do + {:ok, %{sender: sender}} + end + + @doc false + def websocket_handle(frame, _conn_state, state) do + send(state.sender, frame) + {:ok, state} + end + + @doc false + def websocket_info({:text, msg}, _conn_state, state) do + {:reply, {:text, msg}, state} + end + + def websocket_info(:close, _conn_state, _state) do + {:close, <<>>, "done"} + end + + @doc false + def websocket_terminate(_reason, _conn_state, _state) do + :ok + end +end diff --git a/test/support/websub_mock.ex b/test/support/websub_mock.ex new file mode 100644 index 000000000..e3d5a5b16 --- /dev/null +++ b/test/support/websub_mock.ex @@ -0,0 +1,9 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.WebsubMock do + def verify(sub) do + {:ok, sub} + end +end diff --git a/test/tasks/instance.exs b/test/tasks/instance.exs new file mode 100644 index 000000000..6917a2376 --- /dev/null +++ b/test/tasks/instance.exs @@ -0,0 +1,62 @@ +defmodule Pleroma.InstanceTest do + use ExUnit.Case, async: true + + setup do + File.mkdir_p!(tmp_path()) + on_exit(fn -> File.rm_rf(tmp_path()) end) + :ok + end + + defp tmp_path do + "/tmp/generated_files/" + end + + test "running gen" do + mix_task = fn -> + Mix.Tasks.Pleroma.Instance.run([ + "gen", + "--output", + tmp_path() <> "generated_config.exs", + "--output-psql", + tmp_path() <> "setup.psql", + "--domain", + "test.pleroma.social", + "--instance-name", + "Pleroma", + "--admin-email", + "admin@example.com", + "--notify-email", + "notify@example.com", + "--dbhost", + "dbhost", + "--dbname", + "dbname", + "--dbuser", + "dbuser", + "--dbpass", + "dbpass", + "--indexable", + "y" + ]) + end + + ExUnit.CaptureIO.capture_io(fn -> + mix_task.() + end) + + generated_config = File.read!(tmp_path() <> "generated_config.exs") + assert generated_config =~ "host: \"test.pleroma.social\"" + assert generated_config =~ "name: \"Pleroma\"" + assert generated_config =~ "email: \"admin@example.com\"" + assert generated_config =~ "notify_email: \"notify@example.com\"" + assert generated_config =~ "hostname: \"dbhost\"" + assert generated_config =~ "database: \"dbname\"" + assert generated_config =~ "username: \"dbuser\"" + assert generated_config =~ "password: \"dbpass\"" + assert File.read!(tmp_path() <> "setup.psql") == generated_setup_psql() + end + + defp generated_setup_psql do + ~s(CREATE USER dbuser WITH ENCRYPTED PASSWORD 'dbpass';\nCREATE DATABASE dbname OWNER dbuser;\n\\c dbname;\n--Extensions made by ecto.migrate that need superuser access\nCREATE EXTENSION IF NOT EXISTS citext;\nCREATE EXTENSION IF NOT EXISTS pg_trgm;\nCREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";\n) + end +end diff --git a/test/tasks/relay_test.exs b/test/tasks/relay_test.exs new file mode 100644 index 000000000..535dc3756 --- /dev/null +++ b/test/tasks/relay_test.exs @@ -0,0 +1,72 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.RelayTest do + alias Pleroma.Activity + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Relay + alias Pleroma.Web.ActivityPub.Utils + use Pleroma.DataCase + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + + Mix.shell(Mix.Shell.Process) + + on_exit(fn -> + Mix.shell(Mix.Shell.IO) + end) + + :ok + end + + describe "running follow" do + test "relay is followed" do + target_instance = "http://mastodon.example.org/users/admin" + + Mix.Tasks.Pleroma.Relay.run(["follow", target_instance]) + + local_user = Relay.get_actor() + assert local_user.ap_id =~ "/relay" + + target_user = User.get_by_ap_id(target_instance) + refute target_user.local + + activity = Utils.fetch_latest_follow(local_user, target_user) + assert activity.data["type"] == "Follow" + assert activity.data["actor"] == local_user.ap_id + assert activity.data["object"] == target_user.ap_id + end + end + + describe "running unfollow" do + test "relay is unfollowed" do + target_instance = "http://mastodon.example.org/users/admin" + + Mix.Tasks.Pleroma.Relay.run(["follow", target_instance]) + + %User{ap_id: follower_id} = local_user = Relay.get_actor() + target_user = User.get_by_ap_id(target_instance) + follow_activity = Utils.fetch_latest_follow(local_user, target_user) + + Mix.Tasks.Pleroma.Relay.run(["unfollow", target_instance]) + + cancelled_activity = Activity.get_by_ap_id(follow_activity.data["id"]) + assert cancelled_activity.data["state"] == "cancelled" + + [undo_activity] = + ActivityPub.fetch_activities([], %{ + "type" => "Undo", + "actor_id" => follower_id, + "limit" => 1, + "skip_preload" => true + }) + + assert undo_activity.data["type"] == "Undo" + assert undo_activity.data["actor"] == local_user.ap_id + assert undo_activity.data["object"] == cancelled_activity.data + end + end +end diff --git a/test/tasks/uploads_test.exs b/test/tasks/uploads_test.exs new file mode 100644 index 000000000..b0b8eda11 --- /dev/null +++ b/test/tasks/uploads_test.exs @@ -0,0 +1,56 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.UploadsTest do + alias Pleroma.Upload + use Pleroma.DataCase + + import Mock + + setup_all do + Mix.shell(Mix.Shell.Process) + + on_exit(fn -> + Mix.shell(Mix.Shell.IO) + end) + + :ok + end + + describe "running migrate_local" do + test "uploads migrated" do + with_mock Upload, + store: fn %Upload{name: _file, path: _path}, _opts -> {:ok, %{}} end do + Mix.Tasks.Pleroma.Uploads.run(["migrate_local", "S3"]) + + assert_received {:mix_shell, :info, [message]} + assert message =~ "Migrating files from local" + + assert_received {:mix_shell, :info, [message]} + + assert %{"total_count" => total_count} = + Regex.named_captures(~r"^Found (?<total_count>\d+) uploads$", message) + + assert_received {:mix_shell, :info, [message]} + + # @logevery in Mix.Tasks.Pleroma.Uploads + count = + min(50, String.to_integer(total_count)) + |> to_string() + + assert %{"count" => ^count, "total_count" => ^total_count} = + Regex.named_captures( + ~r"^Uploaded (?<count>\d+)/(?<total_count>\d+) files$", + message + ) + end + end + + test "nonexistent uploader" do + assert_raise RuntimeError, ~r/The uploader .* is not an existing/, fn -> + Mix.Tasks.Pleroma.Uploads.run(["migrate_local", "nonexistent"]) + end + end + end +end diff --git a/test/tasks/user_test.exs b/test/tasks/user_test.exs new file mode 100644 index 000000000..242265da5 --- /dev/null +++ b/test/tasks/user_test.exs @@ -0,0 +1,341 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.UserTest do + alias Pleroma.User + use Pleroma.DataCase + + import Pleroma.Factory + import ExUnit.CaptureIO + + setup_all do + Mix.shell(Mix.Shell.Process) + + on_exit(fn -> + Mix.shell(Mix.Shell.IO) + end) + + :ok + end + + describe "running new" do + test "user is created" do + # just get random data + unsaved = build(:user) + + # prepare to answer yes + send(self(), {:mix_shell_input, :yes?, true}) + + Mix.Tasks.Pleroma.User.run([ + "new", + unsaved.nickname, + unsaved.email, + "--name", + unsaved.name, + "--bio", + unsaved.bio, + "--password", + "test", + "--moderator", + "--admin" + ]) + + assert_received {:mix_shell, :info, [message]} + assert message =~ "user will be created" + + assert_received {:mix_shell, :yes?, [message]} + assert message =~ "Continue" + + assert_received {:mix_shell, :info, [message]} + assert message =~ "created" + + user = User.get_by_nickname(unsaved.nickname) + assert user.name == unsaved.name + assert user.email == unsaved.email + assert user.bio == unsaved.bio + assert user.info.is_moderator + assert user.info.is_admin + end + + test "user is not created" do + unsaved = build(:user) + + # prepare to answer no + send(self(), {:mix_shell_input, :yes?, false}) + + Mix.Tasks.Pleroma.User.run(["new", unsaved.nickname, unsaved.email]) + + assert_received {:mix_shell, :info, [message]} + assert message =~ "user will be created" + + assert_received {:mix_shell, :yes?, [message]} + assert message =~ "Continue" + + assert_received {:mix_shell, :info, [message]} + assert message =~ "will not be created" + + refute User.get_by_nickname(unsaved.nickname) + end + end + + describe "running rm" do + test "user is deleted" do + user = insert(:user) + + Mix.Tasks.Pleroma.User.run(["rm", user.nickname]) + + assert_received {:mix_shell, :info, [message]} + assert message =~ " deleted" + + user = User.get_by_nickname(user.nickname) + assert user.info.deactivated + end + + test "no user to delete" do + Mix.Tasks.Pleroma.User.run(["rm", "nonexistent"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No local user" + end + end + + describe "running toggle_activated" do + test "user is deactivated" do + user = insert(:user) + + Mix.Tasks.Pleroma.User.run(["toggle_activated", user.nickname]) + + assert_received {:mix_shell, :info, [message]} + assert message =~ " deactivated" + + user = User.get_by_nickname(user.nickname) + assert user.info.deactivated + end + + test "user is activated" do + user = insert(:user, info: %{deactivated: true}) + + Mix.Tasks.Pleroma.User.run(["toggle_activated", user.nickname]) + + assert_received {:mix_shell, :info, [message]} + assert message =~ " activated" + + user = User.get_by_nickname(user.nickname) + refute user.info.deactivated + end + + test "no user to toggle" do + Mix.Tasks.Pleroma.User.run(["toggle_activated", "nonexistent"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No user" + end + end + + describe "running unsubscribe" do + test "user is unsubscribed" do + followed = insert(:user) + user = insert(:user, %{following: [User.ap_followers(followed)]}) + + Mix.Tasks.Pleroma.User.run(["unsubscribe", user.nickname]) + + assert_received {:mix_shell, :info, [message]} + assert message =~ "Deactivating" + + assert_received {:mix_shell, :info, [message]} + assert message =~ "Unsubscribing" + + # Note that the task has delay :timer.sleep(500) + assert_received {:mix_shell, :info, [message]} + assert message =~ "Successfully unsubscribed" + + user = User.get_by_nickname(user.nickname) + assert Enum.empty?(user.following) + assert user.info.deactivated + end + + test "no user to unsubscribe" do + Mix.Tasks.Pleroma.User.run(["unsubscribe", "nonexistent"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No user" + end + end + + describe "running set" do + test "All statuses set" do + user = insert(:user) + + Mix.Tasks.Pleroma.User.run(["set", user.nickname, "--moderator", "--admin", "--locked"]) + + assert_received {:mix_shell, :info, [message]} + assert message =~ ~r/Moderator status .* true/ + + assert_received {:mix_shell, :info, [message]} + assert message =~ ~r/Locked status .* true/ + + assert_received {:mix_shell, :info, [message]} + assert message =~ ~r/Admin status .* true/ + + user = User.get_by_nickname(user.nickname) + assert user.info.is_moderator + assert user.info.locked + assert user.info.is_admin + end + + test "All statuses unset" do + user = insert(:user, info: %{is_moderator: true, locked: true, is_admin: true}) + + Mix.Tasks.Pleroma.User.run([ + "set", + user.nickname, + "--no-moderator", + "--no-admin", + "--no-locked" + ]) + + assert_received {:mix_shell, :info, [message]} + assert message =~ ~r/Moderator status .* false/ + + assert_received {:mix_shell, :info, [message]} + assert message =~ ~r/Locked status .* false/ + + assert_received {:mix_shell, :info, [message]} + assert message =~ ~r/Admin status .* false/ + + user = User.get_by_nickname(user.nickname) + refute user.info.is_moderator + refute user.info.locked + refute user.info.is_admin + end + + test "no user to set status" do + Mix.Tasks.Pleroma.User.run(["set", "nonexistent", "--moderator"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No local user" + end + end + + describe "running reset_password" do + test "password reset token is generated" do + user = insert(:user) + + assert capture_io(fn -> + Mix.Tasks.Pleroma.User.run(["reset_password", user.nickname]) + end) =~ "URL:" + + assert_received {:mix_shell, :info, [message]} + assert message =~ "Generated" + end + + test "no user to reset password" do + Mix.Tasks.Pleroma.User.run(["reset_password", "nonexistent"]) + + assert_received {:mix_shell, :error, [message]} + assert message =~ "No local user" + end + end + + describe "running invite" do + test "invite token is generated" do + assert capture_io(fn -> + Mix.Tasks.Pleroma.User.run(["invite"]) + end) =~ "http" + + assert_received {:mix_shell, :info, [message]} + assert message =~ "Generated user invite token one time" + end + + test "token is generated with expires_at" do + assert capture_io(fn -> + Mix.Tasks.Pleroma.User.run([ + "invite", + "--expires-at", + Date.to_string(Date.utc_today()) + ]) + end) + + assert_received {:mix_shell, :info, [message]} + assert message =~ "Generated user invite token date limited" + end + + test "token is generated with max use" do + assert capture_io(fn -> + Mix.Tasks.Pleroma.User.run([ + "invite", + "--max-use", + "5" + ]) + end) + + assert_received {:mix_shell, :info, [message]} + assert message =~ "Generated user invite token reusable" + end + + test "token is generated with max use and expires date" do + assert capture_io(fn -> + Mix.Tasks.Pleroma.User.run([ + "invite", + "--max-use", + "5", + "--expires-at", + Date.to_string(Date.utc_today()) + ]) + end) + + assert_received {:mix_shell, :info, [message]} + assert message =~ "Generated user invite token reusable date limited" + end + end + + describe "running invites" do + test "invites are listed" do + {:ok, invite} = Pleroma.UserInviteToken.create_invite() + + {:ok, invite2} = + Pleroma.UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 15}) + + # assert capture_io(fn -> + Mix.Tasks.Pleroma.User.run([ + "invites" + ]) + + # end) + + assert_received {:mix_shell, :info, [message]} + assert_received {:mix_shell, :info, [message2]} + assert_received {:mix_shell, :info, [message3]} + assert message =~ "Invites list:" + assert message2 =~ invite.invite_type + assert message3 =~ invite2.invite_type + end + end + + describe "running revoke_invite" do + test "invite is revoked" do + {:ok, invite} = Pleroma.UserInviteToken.create_invite(%{expires_at: Date.utc_today()}) + + assert capture_io(fn -> + Mix.Tasks.Pleroma.User.run([ + "revoke_invite", + invite.token + ]) + end) + + assert_received {:mix_shell, :info, [message]} + assert message =~ "Invite for token #{invite.token} was revoked." + end + end + + describe "running delete_activities" do + test "activities are deleted" do + %{nickname: nickname} = insert(:user) + + assert :ok == Mix.Tasks.Pleroma.User.run(["delete_activities", nickname]) + assert_received {:mix_shell, :info, [message]} + assert message == "User #{nickname} statuses deleted." + end + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs index 94ba68ff8..f604ba63d 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + ExUnit.start() Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual) diff --git a/test/upload_test.exs b/test/upload_test.exs index b2ce755d2..946ebcb5a 100644 --- a/test/upload_test.exs +++ b/test/upload_test.exs @@ -1,24 +1,13 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.UploadTest do alias Pleroma.Upload use Pleroma.DataCase describe "Storing a file with the Local uploader" do - setup do - uploader = Pleroma.Config.get([Pleroma.Upload, :uploader]) - filters = Pleroma.Config.get([Pleroma.Upload, :filters]) - - unless uploader == Pleroma.Uploaders.Local || filters != [] do - Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) - Pleroma.Config.put([Pleroma.Upload, :filters], []) - - on_exit(fn -> - Pleroma.Config.put([Pleroma.Upload, :uploader], uploader) - Pleroma.Config.put([Pleroma.Upload, :filters], filters) - end) - end - - :ok - end + setup [:ensure_local_uploader] test "returns a media url" do File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") @@ -67,7 +56,7 @@ defmodule Pleroma.UploadTest do assert List.first(data["url"])["href"] == Pleroma.Web.base_url() <> - "/media/e7a6d0cf595bff76f14c9a98b6c199539559e8b844e02e51e5efcfd1f614a2df.jpg" + "/media/e30397b58d226d6583ab5b8b3c5defb0c682bda5c31ef07a9f57c1c4986e3781.jpg" end test "copies the file to the configured folder without deduping" do @@ -148,5 +137,36 @@ defmodule Pleroma.UploadTest do refute data["name"] == "an [image.jpg" end + + test "escapes invalid characters in url" do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: "an… image.jpg" + } + + {:ok, data} = Upload.store(file) + [attachment_url | _] = data["url"] + + assert Path.basename(attachment_url["href"]) == "an%E2%80%A6%20image.jpg" + end + + test "escapes reserved uri characters" do + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: ":?#[]@!$&\\'()*+,;=.jpg" + } + + {:ok, data} = Upload.store(file) + [attachment_url | _] = data["url"] + + assert Path.basename(attachment_url["href"]) == + "%3A%3F%23%5B%5D%40%21%24%26%5C%27%28%29%2A%2B%2C%3B%3D.jpg" + end end end diff --git a/test/user_invite_token_test.exs b/test/user_invite_token_test.exs new file mode 100644 index 000000000..276788254 --- /dev/null +++ b/test/user_invite_token_test.exs @@ -0,0 +1,96 @@ +defmodule Pleroma.UserInviteTokenTest do + use ExUnit.Case, async: true + use Pleroma.DataCase + alias Pleroma.UserInviteToken + + describe "valid_invite?/1 one time invites" do + setup do + invite = %UserInviteToken{invite_type: "one_time"} + + {:ok, invite: invite} + end + + test "not used returns true", %{invite: invite} do + invite = %{invite | used: false} + assert UserInviteToken.valid_invite?(invite) + end + + test "used returns false", %{invite: invite} do + invite = %{invite | used: true} + refute UserInviteToken.valid_invite?(invite) + end + end + + describe "valid_invite?/1 reusable invites" do + setup do + invite = %UserInviteToken{ + invite_type: "reusable", + max_use: 5 + } + + {:ok, invite: invite} + end + + test "with less uses then max use returns true", %{invite: invite} do + invite = %{invite | uses: 4} + assert UserInviteToken.valid_invite?(invite) + end + + test "with equal or more uses then max use returns false", %{invite: invite} do + invite = %{invite | uses: 5} + + refute UserInviteToken.valid_invite?(invite) + + invite = %{invite | uses: 6} + + refute UserInviteToken.valid_invite?(invite) + end + end + + describe "valid_token?/1 date limited invites" do + setup do + invite = %UserInviteToken{invite_type: "date_limited"} + {:ok, invite: invite} + end + + test "expires today returns true", %{invite: invite} do + invite = %{invite | expires_at: Date.utc_today()} + assert UserInviteToken.valid_invite?(invite) + end + + test "expires yesterday returns false", %{invite: invite} do + invite = %{invite | expires_at: Date.add(Date.utc_today(), -1)} + invite = Repo.insert!(invite) + refute UserInviteToken.valid_invite?(invite) + end + end + + describe "valid_token?/1 reusable date limited invites" do + setup do + invite = %UserInviteToken{invite_type: "reusable_date_limited", max_use: 5} + {:ok, invite: invite} + end + + test "not overdue date and less uses returns true", %{invite: invite} do + invite = %{invite | expires_at: Date.utc_today(), uses: 4} + assert UserInviteToken.valid_invite?(invite) + end + + test "overdue date and less uses returns false", %{invite: invite} do + invite = %{invite | expires_at: Date.add(Date.utc_today(), -1)} + invite = Repo.insert!(invite) + refute UserInviteToken.valid_invite?(invite) + end + + test "not overdue date with more uses returns false", %{invite: invite} do + invite = %{invite | expires_at: Date.utc_today(), uses: 5} + refute UserInviteToken.valid_invite?(invite) + end + + test "overdue date with more uses returns false", %{invite: invite} do + invite = %{invite | expires_at: Date.add(Date.utc_today(), -1), uses: 5} + invite = Repo.insert!(invite) + refute UserInviteToken.valid_invite?(invite) + end + end +end diff --git a/test/user_test.exs b/test/user_test.exs index 62104df90..d2167a970 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -1,13 +1,39 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.UserTest do + alias Pleroma.Activity alias Pleroma.Builders.UserBuilder - alias Pleroma.{User, Repo, Activity} - alias Pleroma.Web.OStatus - alias Pleroma.Web.Websub.WebsubClientSubscription + alias Pleroma.Repo + alias Pleroma.User alias Pleroma.Web.CommonAPI + use Pleroma.DataCase import Pleroma.Factory - import Ecto.Query + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + describe "when tags are nil" do + test "tagging a user" do + user = insert(:user, %{tags: nil}) + user = User.tag(user, ["cool", "dude"]) + + assert "cool" in user.tags + assert "dude" in user.tags + end + + test "untagging a user" do + user = insert(:user, %{tags: nil}) + user = User.untag(user, ["cool", "dude"]) + + assert user.tags == [] + end + end test "ap_id returns the activity pub id for the user" do user = UserBuilder.build() @@ -25,13 +51,78 @@ defmodule Pleroma.UserTest do assert expected_followers_collection == User.ap_followers(user) end + test "returns all pending follow requests" do + unlocked = insert(:user) + locked = insert(:user, %{info: %{locked: true}}) + follower = insert(:user) + + Pleroma.Web.TwitterAPI.TwitterAPI.follow(follower, %{"user_id" => unlocked.id}) + Pleroma.Web.TwitterAPI.TwitterAPI.follow(follower, %{"user_id" => locked.id}) + + assert {:ok, []} = User.get_follow_requests(unlocked) + assert {:ok, [activity]} = User.get_follow_requests(locked) + + assert activity + end + + test "doesn't return already accepted or duplicate follow requests" do + locked = insert(:user, %{info: %{locked: true}}) + pending_follower = insert(:user) + accepted_follower = insert(:user) + + Pleroma.Web.TwitterAPI.TwitterAPI.follow(pending_follower, %{"user_id" => locked.id}) + Pleroma.Web.TwitterAPI.TwitterAPI.follow(pending_follower, %{"user_id" => locked.id}) + Pleroma.Web.TwitterAPI.TwitterAPI.follow(accepted_follower, %{"user_id" => locked.id}) + User.maybe_follow(accepted_follower, locked) + + assert {:ok, [activity]} = User.get_follow_requests(locked) + assert activity + end + + test "follow_all follows mutliple users" do + user = insert(:user) + followed_zero = insert(:user) + followed_one = insert(:user) + followed_two = insert(:user) + blocked = insert(:user) + not_followed = insert(:user) + reverse_blocked = insert(:user) + + {:ok, user} = User.block(user, blocked) + {:ok, reverse_blocked} = User.block(reverse_blocked, user) + + {:ok, user} = User.follow(user, followed_zero) + + {:ok, user} = User.follow_all(user, [followed_one, followed_two, blocked, reverse_blocked]) + + assert User.following?(user, followed_one) + assert User.following?(user, followed_two) + assert User.following?(user, followed_zero) + refute User.following?(user, not_followed) + refute User.following?(user, blocked) + refute User.following?(user, reverse_blocked) + end + + test "follow_all follows mutliple users without duplicating" do + user = insert(:user) + followed_zero = insert(:user) + followed_one = insert(:user) + followed_two = insert(:user) + + {:ok, user} = User.follow_all(user, [followed_zero, followed_one]) + assert length(user.following) == 3 + + {:ok, user} = User.follow_all(user, [followed_one, followed_two]) + assert length(user.following) == 4 + end + test "follow takes a user and another user" do user = insert(:user) followed = insert(:user) {:ok, user} = User.follow(user, followed) - user = Repo.get(User, user.id) + user = User.get_by_id(user.id) followed = User.get_by_ap_id(followed.ap_id) assert followed.info.follower_count == 1 @@ -55,6 +146,15 @@ defmodule Pleroma.UserTest do {:error, _} = User.follow(blockee, blocker) end + test "can't subscribe to a user who blocked us" do + blocker = insert(:user) + blocked = insert(:user) + + {:ok, blocker} = User.block(blocker, blocked) + + {:error, _} = User.subscribe(blocked, blocker) + end + test "local users do not automatically follow local locked accounts" do follower = insert(:user, info: %{locked: true}) followed = insert(:user, info: %{locked: true}) @@ -87,7 +187,7 @@ defmodule Pleroma.UserTest do {:ok, user, _activity} = User.unfollow(user, followed) - user = Repo.get(User, user.id) + user = User.get_by_id(user.id) assert user.following == [] end @@ -97,7 +197,7 @@ defmodule Pleroma.UserTest do {:error, _} = User.unfollow(user, user) - user = Repo.get(User, user.id) + user = User.get_by_id(user.id) assert user.following == [user.ap_id] end @@ -109,6 +209,13 @@ defmodule Pleroma.UserTest do refute User.following?(followed, user) end + test "fetches correct profile for nickname beginning with number" do + # Use old-style integer ID to try to reproduce the problem + user = insert(:user, %{id: 1080}) + userwithnumbers = insert(:user, %{nickname: "#{user.id}garbage"}) + assert userwithnumbers == User.get_cached_by_nickname_or_id(userwithnumbers.nickname) + end + describe "user registration" do @full_user_data %{ bio: "A guy", @@ -119,6 +226,43 @@ defmodule Pleroma.UserTest do email: "email@example.com" } + test "it autofollows accounts that are set for it" do + user = insert(:user) + remote_user = insert(:user, %{local: false}) + + Pleroma.Config.put([:instance, :autofollowed_nicknames], [ + user.nickname, + remote_user.nickname + ]) + + cng = User.register_changeset(%User{}, @full_user_data) + + {:ok, registered_user} = User.register(cng) + + assert User.following?(registered_user, user) + refute User.following?(registered_user, remote_user) + + Pleroma.Config.put([:instance, :autofollowed_nicknames], []) + end + + test "it sends a welcome message if it is set" do + welcome_user = insert(:user) + + Pleroma.Config.put([:instance, :welcome_user_nickname], welcome_user.nickname) + Pleroma.Config.put([:instance, :welcome_message], "Hello, this is a cool site") + + cng = User.register_changeset(%User{}, @full_user_data) + {:ok, registered_user} = User.register(cng) + + activity = Repo.one(Pleroma.Activity) + assert registered_user.ap_id in activity.recipients + assert activity.data["object"]["content"] =~ "cool site" + assert activity.actor == welcome_user.ap_id + + Pleroma.Config.put([:instance, :welcome_user_nickname], nil) + Pleroma.Config.put([:instance, :welcome_message], nil) + end + test "it requires an email, name, nickname and password, bio is optional" do @full_user_data |> Map.keys() @@ -130,6 +274,20 @@ defmodule Pleroma.UserTest do end) end + test "it restricts certain nicknames" do + [restricted_name | _] = Pleroma.Config.get([Pleroma.User, :restricted_nicknames]) + + assert is_bitstring(restricted_name) + + params = + @full_user_data + |> Map.put(:nickname, restricted_name) + + changeset = User.register_changeset(%User{}, params) + + refute changeset.valid? + end + test "it sets the password_hash, ap_id and following fields" do changeset = User.register_changeset(%User{}, @full_user_data) @@ -144,6 +302,86 @@ defmodule Pleroma.UserTest do assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers" end + + test "it ensures info is not nil" do + changeset = User.register_changeset(%User{}, @full_user_data) + + assert changeset.valid? + + {:ok, user} = + changeset + |> Repo.insert() + + refute is_nil(user.info) + end + end + + describe "user registration, with :account_activation_required" do + @full_user_data %{ + bio: "A guy", + name: "my name", + nickname: "nick", + password: "test", + password_confirmation: "test", + email: "email@example.com" + } + + setup do + setting = Pleroma.Config.get([:instance, :account_activation_required]) + + unless setting do + Pleroma.Config.put([:instance, :account_activation_required], true) + on_exit(fn -> Pleroma.Config.put([:instance, :account_activation_required], setting) end) + end + + :ok + end + + test "it creates unconfirmed user" do + changeset = User.register_changeset(%User{}, @full_user_data) + assert changeset.valid? + + {:ok, user} = Repo.insert(changeset) + + assert user.info.confirmation_pending + assert user.info.confirmation_token + end + + test "it creates confirmed user if :confirmed option is given" do + changeset = User.register_changeset(%User{}, @full_user_data, confirmed: true) + assert changeset.valid? + + {:ok, user} = Repo.insert(changeset) + + refute user.info.confirmation_pending + refute user.info.confirmation_token + end + end + + describe "get_or_fetch/1" do + test "gets an existing user by nickname" do + user = insert(:user) + fetched_user = User.get_or_fetch(user.nickname) + + assert user == fetched_user + end + + test "gets an existing user by ap_id" do + ap_id = "http://mastodon.example.org/users/admin" + + user = + insert( + :user, + local: false, + nickname: "admin@mastodon.example.org", + ap_id: ap_id, + info: %{} + ) + + fetched_user = User.get_or_fetch(ap_id) + freshed_user = refresh_record(user) + assert freshed_user == fetched_user + end end describe "fetching a user from nickname or trying to build one" do @@ -161,6 +399,24 @@ defmodule Pleroma.UserTest do assert user == fetched_user end + test "gets an existing user by fully qualified nickname" do + user = insert(:user) + + fetched_user = + User.get_or_fetch_by_nickname(user.nickname <> "@" <> Pleroma.Web.Endpoint.host()) + + assert user == fetched_user + end + + test "gets an existing user by fully qualified nickname, case insensitive" do + user = insert(:user, nickname: "nick") + casing_altered_fqn = String.upcase(user.nickname <> "@" <> Pleroma.Web.Endpoint.host()) + + fetched_user = User.get_or_fetch_by_nickname(casing_altered_fqn) + + assert user == fetched_user + end + test "fetches an external user via ostatus if no user exists" do fetched_user = User.get_or_fetch_by_nickname("shp@social.heldscal.la") assert fetched_user.nickname == "shp@social.heldscal.la" @@ -368,6 +624,44 @@ defmodule Pleroma.UserTest do end end + describe "follow_import" do + test "it imports user followings from list" do + [user1, user2, user3] = insert_list(3, :user) + + identifiers = [ + user2.ap_id, + user3.nickname + ] + + result = User.follow_import(user1, identifiers) + assert is_list(result) + assert result == [user2, user3] + end + end + + describe "mutes" do + test "it mutes people" do + user = insert(:user) + muted_user = insert(:user) + + refute User.mutes?(user, muted_user) + + {:ok, user} = User.mute(user, muted_user) + + assert User.mutes?(user, muted_user) + end + + test "it unmutes users" do + user = insert(:user) + muted_user = insert(:user) + + {:ok, user} = User.mute(user, muted_user) + {:ok, user} = User.unmute(user, muted_user) + + refute User.mutes?(user, muted_user) + end + end + describe "blocks" do test "it blocks people" do user = insert(:user) @@ -401,7 +695,7 @@ defmodule Pleroma.UserTest do assert User.following?(blocked, blocker) {:ok, blocker} = User.block(blocker, blocked) - blocked = Repo.get(User, blocked.id) + blocked = User.get_by_id(blocked.id) assert User.blocks?(blocker, blocked) @@ -419,7 +713,7 @@ defmodule Pleroma.UserTest do refute User.following?(blocked, blocker) {:ok, blocker} = User.block(blocker, blocked) - blocked = Repo.get(User, blocked.id) + blocked = User.get_by_id(blocked.id) assert User.blocks?(blocker, blocked) @@ -437,13 +731,29 @@ defmodule Pleroma.UserTest do assert User.following?(blocked, blocker) {:ok, blocker} = User.block(blocker, blocked) - blocked = Repo.get(User, blocked.id) + blocked = User.get_by_id(blocked.id) assert User.blocks?(blocker, blocked) refute User.following?(blocker, blocked) refute User.following?(blocked, blocker) end + + test "blocks tear down blocked->blocker subscription relationships" do + blocker = insert(:user) + blocked = insert(:user) + + {:ok, blocker} = User.subscribe(blocked, blocker) + + assert User.subscribed_to?(blocked, blocker) + refute User.subscribed_to?(blocker, blocked) + + {:ok, blocker} = User.block(blocker, blocked) + + assert User.blocks?(blocker, blocked) + refute User.subscribed_to?(blocker, blocked) + refute User.subscribed_to?(blocked, blocker) + end end describe "domain blocking" do @@ -467,6 +777,21 @@ defmodule Pleroma.UserTest do end end + describe "blocks_import" do + test "it imports user blocks from list" do + [user1, user2, user3] = insert_list(3, :user) + + identifiers = [ + user2.ap_id, + user3.nickname + ] + + result = User.blocks_import(user1, identifiers) + assert is_list(result) + assert result == [user2, user3] + end + end + test "get recipients from activity" do actor = insert(:user) user = insert(:user, local: true) @@ -479,12 +804,13 @@ defmodule Pleroma.UserTest do "status" => "hey @#{addressed.nickname} @#{addressed_remote.nickname}" }) - assert [addressed] == User.get_recipients_from_activity(activity) + assert Enum.map([actor, addressed], & &1.ap_id) -- + Enum.map(User.get_recipients_from_activity(activity), & &1.ap_id) == [] {:ok, user} = User.follow(user, actor) {:ok, _user_two} = User.follow(user_two, actor) recipients = User.get_recipients_from_activity(activity) - assert length(recipients) == 2 + assert length(recipients) == 3 assert user in recipients assert addressed in recipients end @@ -498,6 +824,16 @@ defmodule Pleroma.UserTest do assert false == user.info.deactivated end + test ".delete_user_activities deletes all create activities" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "2hu"}) + {:ok, _} = User.delete_user_activities(user) + + # TODO: Remove favorites, repeats, delete activities. + refute Activity.get_by_id(activity.id) + end + test ".delete deactivates a user, all follow relationships and all create activities" do user = insert(:user) followed = insert(:user) @@ -515,9 +851,9 @@ defmodule Pleroma.UserTest do {:ok, _} = User.delete(user) - followed = Repo.get(User, followed.id) - follower = Repo.get(User, follower.id) - user = Repo.get(User, user.id) + followed = User.get_by_id(followed.id) + follower = User.get_by_id(follower.id) + user = User.get_by_id(user.id) assert user.info.deactivated @@ -526,7 +862,7 @@ defmodule Pleroma.UserTest do # TODO: Remove favorites, repeats, delete activities. - refute Repo.get(Activity, activity.id) + refute Activity.get_by_id(activity.id) end test "get_public_key_for_ap_id fetches a user that's not in the db" do @@ -541,10 +877,10 @@ defmodule Pleroma.UserTest do end describe "per-user rich-text filtering" do - test "html_filter_policy returns nil when rich-text is enabled" do + test "html_filter_policy returns default policies, when rich-text is enabled" do user = insert(:user) - assert nil == User.html_filter_policy(user) + assert Pleroma.Config.get([:markup, :scrub_policy]) == User.html_filter_policy(user) end test "html_filter_policy returns TwitterText scrubber when rich-text is disabled" do @@ -557,7 +893,7 @@ defmodule Pleroma.UserTest do describe "caching" do test "invalidate_cache works" do user = insert(:user) - user_info = User.get_cached_user_info(user) + _user_info = User.get_cached_user_info(user) User.invalidate_cache(user) @@ -582,14 +918,253 @@ defmodule Pleroma.UserTest do end describe "User.search" do - test "finds a user, ranking by similarity" do - user = insert(:user, %{name: "lain"}) - user_two = insert(:user, %{name: "ean"}) - user_three = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social"}) - user_four = insert(:user, %{nickname: "lain@pleroma.soykaf.com"}) + test "finds a user by full or partial nickname" do + user = insert(:user, %{nickname: "john"}) + + Enum.each(["john", "jo", "j"], fn query -> + assert user == + User.search(query) + |> List.first() + |> Map.put(:search_rank, nil) + |> Map.put(:search_type, nil) + end) + end + + test "finds a user by full or partial name" do + user = insert(:user, %{name: "John Doe"}) + + Enum.each(["John Doe", "JOHN", "doe", "j d", "j", "d"], fn query -> + assert user == + User.search(query) + |> List.first() + |> Map.put(:search_rank, nil) + |> Map.put(:search_type, nil) + end) + end + + test "finds users, preferring nickname matches over name matches" do + u1 = insert(:user, %{name: "lain", nickname: "nick1"}) + u2 = insert(:user, %{nickname: "lain", name: "nick1"}) + + assert [u2.id, u1.id] == Enum.map(User.search("lain"), & &1.id) + end + + test "finds users, considering density of matched tokens" do + u1 = insert(:user, %{name: "Bar Bar plus Word Word"}) + u2 = insert(:user, %{name: "Word Word Bar Bar Bar"}) + + assert [u2.id, u1.id] == Enum.map(User.search("bar word"), & &1.id) + end + + test "finds users, ranking by similarity" do + u1 = insert(:user, %{name: "lain"}) + _u2 = insert(:user, %{name: "ean"}) + u3 = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social"}) + u4 = insert(:user, %{nickname: "lain@pleroma.soykaf.com"}) + + assert [u4.id, u3.id, u1.id] == Enum.map(User.search("lain@ple"), & &1.id) + end + + test "finds users, handling misspelled requests" do + u1 = insert(:user, %{name: "lain"}) + + assert [u1.id] == Enum.map(User.search("laiin"), & &1.id) + end + + test "finds users, boosting ranks of friends and followers" do + u1 = insert(:user) + u2 = insert(:user, %{name: "Doe"}) + follower = insert(:user, %{name: "Doe"}) + friend = insert(:user, %{name: "Doe"}) + + {:ok, follower} = User.follow(follower, u1) + {:ok, u1} = User.follow(u1, friend) + + assert [friend.id, follower.id, u2.id] -- + Enum.map(User.search("doe", resolve: false, for_user: u1), & &1.id) == [] + end + + test "finds a user whose name is nil" do + _user = insert(:user, %{name: "notamatch", nickname: "testuser@pleroma.amplifie.red"}) + user_two = insert(:user, %{name: nil, nickname: "lain@pleroma.soykaf.com"}) + + assert user_two == + User.search("lain@pleroma.soykaf.com") + |> List.first() + |> Map.put(:search_rank, nil) + |> Map.put(:search_type, nil) + end + + test "does not yield false-positive matches" do + insert(:user, %{name: "John Doe"}) + + Enum.each(["mary", "a", ""], fn query -> + assert [] == User.search(query) + end) + end + + test "works with URIs" do + results = User.search("http://mastodon.example.org/users/admin", resolve: true) + result = results |> List.first() + + user = User.get_by_ap_id("http://mastodon.example.org/users/admin") + + assert length(results) == 1 + assert user == result |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil) + end + end - assert user_four == - User.search("lain@ple") |> List.first() |> Map.put(:search_distance, nil) + test "auth_active?/1 works correctly" do + Pleroma.Config.put([:instance, :account_activation_required], true) + + local_user = insert(:user, local: true, info: %{confirmation_pending: true}) + confirmed_user = insert(:user, local: true, info: %{confirmation_pending: false}) + remote_user = insert(:user, local: false) + + refute User.auth_active?(local_user) + assert User.auth_active?(confirmed_user) + assert User.auth_active?(remote_user) + + Pleroma.Config.put([:instance, :account_activation_required], false) + end + + describe "superuser?/1" do + test "returns false for unprivileged users" do + user = insert(:user, local: true) + + refute User.superuser?(user) + end + + test "returns false for remote users" do + user = insert(:user, local: false) + remote_admin_user = insert(:user, local: false, info: %{is_admin: true}) + + refute User.superuser?(user) + refute User.superuser?(remote_admin_user) + end + + test "returns true for local moderators" do + user = insert(:user, local: true, info: %{is_moderator: true}) + + assert User.superuser?(user) + end + + test "returns true for local admins" do + user = insert(:user, local: true, info: %{is_admin: true}) + + assert User.superuser?(user) + end + end + + describe "visible_for?/2" do + test "returns true when the account is itself" do + user = insert(:user, local: true) + + assert User.visible_for?(user, user) + end + + test "returns false when the account is unauthenticated and auth is required" do + Pleroma.Config.put([:instance, :account_activation_required], true) + + user = insert(:user, local: true, info: %{confirmation_pending: true}) + other_user = insert(:user, local: true) + + refute User.visible_for?(user, other_user) + + Pleroma.Config.put([:instance, :account_activation_required], false) + end + + test "returns true when the account is unauthenticated and auth is not required" do + user = insert(:user, local: true, info: %{confirmation_pending: true}) + other_user = insert(:user, local: true) + + assert User.visible_for?(user, other_user) + end + + test "returns true when the account is unauthenticated and being viewed by a privileged account (auth required)" do + Pleroma.Config.put([:instance, :account_activation_required], true) + + user = insert(:user, local: true, info: %{confirmation_pending: true}) + other_user = insert(:user, local: true, info: %{is_admin: true}) + + assert User.visible_for?(user, other_user) + + Pleroma.Config.put([:instance, :account_activation_required], false) end end + + describe "parse_bio/2" do + test "preserves hosts in user links text" do + remote_user = insert(:user, local: false, nickname: "nick@domain.com") + user = insert(:user) + bio = "A.k.a. @nick@domain.com" + + expected_text = + "A.k.a. <span class='h-card'><a data-user='#{remote_user.id}' class='u-url mention' href='#{ + remote_user.ap_id + }'>" <> "@<span>nick@domain.com</span></a></span>" + + assert expected_text == User.parse_bio(bio, user) + end + + test "Adds rel=me on linkbacked urls" do + user = insert(:user, ap_id: "http://social.example.org/users/lain") + + bio = "http://example.org/rel_me/null" + expected_text = "<a href=\"#{bio}\">#{bio}</a>" + assert expected_text == User.parse_bio(bio, user) + + bio = "http://example.org/rel_me/link" + expected_text = "<a href=\"#{bio}\">#{bio}</a>" + assert expected_text == User.parse_bio(bio, user) + + bio = "http://example.org/rel_me/anchor" + expected_text = "<a href=\"#{bio}\">#{bio}</a>" + assert expected_text == User.parse_bio(bio, user) + end + end + + test "bookmarks" do + user = insert(:user) + + {:ok, activity1} = + CommonAPI.post(user, %{ + "status" => "heweoo!" + }) + + id1 = activity1.data["object"]["id"] + + {:ok, activity2} = + CommonAPI.post(user, %{ + "status" => "heweoo!" + }) + + id2 = activity2.data["object"]["id"] + + assert {:ok, user_state1} = User.bookmark(user, id1) + assert user_state1.bookmarks == [id1] + + assert {:ok, user_state2} = User.unbookmark(user, id1) + assert user_state2.bookmarks == [] + + assert {:ok, user_state3} = User.bookmark(user, id2) + assert user_state3.bookmarks == [id2] + end + + test "follower count is updated when a follower is blocked" do + user = insert(:user) + follower = insert(:user) + follower2 = insert(:user) + follower3 = insert(:user) + + {:ok, follower} = Pleroma.User.follow(follower, user) + {:ok, _follower2} = Pleroma.User.follow(follower2, user) + {:ok, _follower3} = Pleroma.User.follow(follower3, user) + + {:ok, _} = Pleroma.User.block(user, follower) + + user_show = Pleroma.Web.TwitterAPI.UserView.render("show.json", %{user: user}) + + assert Map.get(user_show, "followers_count") == 2 + end end diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index 1c24b348c..7b1c60f15 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -1,9 +1,21 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do use Pleroma.Web.ConnCase import Pleroma.Factory - alias Pleroma.Web.ActivityPub.{UserView, ObjectView} - alias Pleroma.{Repo, User} alias Pleroma.Activity + alias Pleroma.Instances + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ObjectView + alias Pleroma.Web.ActivityPub.UserView + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end describe "/relay" do test "with the relay active, it returns the relay user", %{conn: conn} do @@ -18,17 +30,34 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do test "with the relay disabled, it returns 404", %{conn: conn} do Pleroma.Config.put([:instance, :allow_relay], false) - res = - conn - |> get(activity_pub_path(conn, :relay)) - |> json_response(404) + conn + |> get(activity_pub_path(conn, :relay)) + |> json_response(404) + |> assert Pleroma.Config.put([:instance, :allow_relay], true) end end describe "/users/:nickname" do - test "it returns a json representation of the user", %{conn: conn} do + test "it returns a json representation of the user with accept application/json", %{ + conn: conn + } do + user = insert(:user) + + conn = + conn + |> put_req_header("accept", "application/json") + |> get("/users/#{user.nickname}") + + user = User.get_by_id(user.id) + + assert json_response(conn, 200) == UserView.render("user.json", %{user: user}) + end + + test "it returns a json representation of the user with accept application/activity+json", %{ + conn: conn + } do user = insert(:user) conn = @@ -36,14 +65,47 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do |> put_req_header("accept", "application/activity+json") |> get("/users/#{user.nickname}") - user = Repo.get(User, user.id) + user = User.get_by_id(user.id) + + assert json_response(conn, 200) == UserView.render("user.json", %{user: user}) + end + + test "it returns a json representation of the user with accept application/ld+json", %{ + conn: conn + } do + user = insert(:user) + + conn = + conn + |> put_req_header( + "accept", + "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" + ) + |> get("/users/#{user.nickname}") + + user = User.get_by_id(user.id) assert json_response(conn, 200) == UserView.render("user.json", %{user: user}) end end describe "/object/:uuid" do - test "it returns a json representation of the object", %{conn: conn} do + test "it returns a json representation of the object with accept application/json", %{ + conn: conn + } do + note = insert(:note) + uuid = String.split(note.data["id"], "/") |> List.last() + + conn = + conn + |> put_req_header("accept", "application/json") + |> get("/objects/#{uuid}") + + assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note}) + end + + test "it returns a json representation of the object with accept application/activity+json", + %{conn: conn} do note = insert(:note) uuid = String.split(note.data["id"], "/") |> List.last() @@ -55,6 +117,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note}) end + test "it returns a json representation of the object with accept application/ld+json", %{ + conn: conn + } do + note = insert(:note) + uuid = String.split(note.data["id"], "/") |> List.last() + + conn = + conn + |> put_req_header( + "accept", + "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" + ) + |> get("/objects/#{uuid}") + + assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note}) + end + test "it returns 404 for non-public messages", %{conn: conn} do note = insert(:direct_note) uuid = String.split(note.data["id"], "/") |> List.last() @@ -66,6 +145,59 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert json_response(conn, 404) end + + test "it returns 404 for tombstone objects", %{conn: conn} do + tombstone = insert(:tombstone) + uuid = String.split(tombstone.data["id"], "/") |> List.last() + + conn = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/objects/#{uuid}") + + assert json_response(conn, 404) + end + end + + describe "/object/:uuid/likes" do + test "it returns the like activities in a collection", %{conn: conn} do + like = insert(:like_activity) + uuid = String.split(like.data["object"], "/") |> List.last() + + result = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/objects/#{uuid}/likes") + |> json_response(200) + + assert List.first(result["first"]["orderedItems"])["id"] == like.data["id"] + end + end + + describe "/activities/:uuid" do + test "it returns a json representation of the activity", %{conn: conn} do + activity = insert(:note_activity) + uuid = String.split(activity.data["id"], "/") |> List.last() + + conn = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/activities/#{uuid}") + + assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity}) + end + + test "it returns 404 for non-public activities", %{conn: conn} do + activity = insert(:direct_note_activity) + uuid = String.split(activity.data["id"], "/") |> List.last() + + conn = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/activities/#{uuid}") + + assert json_response(conn, 404) + end end describe "/inbox" do @@ -82,6 +214,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do :timer.sleep(500) assert Activity.get_by_ap_id(data["id"]) end + + test "it clears `unreachable` federation status of the sender", %{conn: conn} do + data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() + + sender_url = data["actor"] + Instances.set_consistently_unreachable(sender_url) + refute Instances.reachable?(sender_url) + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/inbox", data) + + assert "ok" == json_response(conn, 200) + assert Instances.reachable?(sender_url) + end end describe "/users/:nickname/inbox" do @@ -103,9 +252,99 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do :timer.sleep(500) assert Activity.get_by_ap_id(data["id"]) end + + test "it accepts messages from actors that are followed by the user", %{conn: conn} do + recipient = insert(:user) + actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"}) + + {:ok, recipient} = User.follow(recipient, actor) + + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + + object = + data["object"] + |> Map.put("attributedTo", actor.ap_id) + + data = + data + |> Map.put("actor", actor.ap_id) + |> Map.put("object", object) + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{recipient.nickname}/inbox", data) + + assert "ok" == json_response(conn, 200) + :timer.sleep(500) + assert Activity.get_by_ap_id(data["id"]) + end + + test "it rejects reads from other users", %{conn: conn} do + user = insert(:user) + otheruser = insert(:user) + + conn = + conn + |> assign(:user, otheruser) + |> put_req_header("accept", "application/activity+json") + |> get("/users/#{user.nickname}/inbox") + + assert json_response(conn, 403) + end + + test "it returns a note activity in a collection", %{conn: conn} do + note_activity = insert(:direct_note_activity) + user = User.get_cached_by_ap_id(hd(note_activity.data["to"])) + + conn = + conn + |> assign(:user, user) + |> put_req_header("accept", "application/activity+json") + |> get("/users/#{user.nickname}/inbox") + + assert response(conn, 200) =~ note_activity.data["object"]["content"] + end + + test "it clears `unreachable` federation status of the sender", %{conn: conn} do + user = insert(:user) + + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + |> Map.put("bcc", [user.ap_id]) + + sender_host = URI.parse(data["actor"]).host + Instances.set_consistently_unreachable(sender_host) + refute Instances.reachable?(sender_host) + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/inbox", data) + + assert "ok" == json_response(conn, 200) + assert Instances.reachable?(sender_host) + end end describe "/users/:nickname/outbox" do + test "it will not bomb when there is no activity", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/users/#{user.nickname}/outbox") + + result = json_response(conn, 200) + assert user.ap_id <> "/outbox" == result["id"] + end + test "it returns a note activity in a collection", %{conn: conn} do note_activity = insert(:note_activity) user = User.get_cached_by_ap_id(note_activity.data["actor"]) @@ -129,6 +368,121 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert response(conn, 200) =~ announce_activity.data["object"] end + + test "it rejects posts from other users", %{conn: conn} do + data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!() + user = insert(:user) + otheruser = insert(:user) + + conn = + conn + |> assign(:user, otheruser) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/outbox", data) + + assert json_response(conn, 403) + end + + test "it inserts an incoming create activity into the database", %{conn: conn} do + data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!() + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/outbox", data) + + result = json_response(conn, 201) + assert Activity.get_by_ap_id(result["id"]) + end + + test "it rejects an incoming activity with bogus type", %{conn: conn} do + data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!() + user = insert(:user) + + data = + data + |> Map.put("type", "BadType") + + conn = + conn + |> assign(:user, user) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/outbox", data) + + assert json_response(conn, 400) + end + + test "it erects a tombstone when receiving a delete activity", %{conn: conn} do + note_activity = insert(:note_activity) + user = User.get_cached_by_ap_id(note_activity.data["actor"]) + + data = %{ + type: "Delete", + object: %{ + id: note_activity.data["object"]["id"] + } + } + + conn = + conn + |> assign(:user, user) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/outbox", data) + + result = json_response(conn, 201) + assert Activity.get_by_ap_id(result["id"]) + + object = Object.get_by_ap_id(note_activity.data["object"]["id"]) + assert object + assert object.data["type"] == "Tombstone" + end + + test "it rejects delete activity of object from other actor", %{conn: conn} do + note_activity = insert(:note_activity) + user = insert(:user) + + data = %{ + type: "Delete", + object: %{ + id: note_activity.data["object"]["id"] + } + } + + conn = + conn + |> assign(:user, user) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/outbox", data) + + assert json_response(conn, 400) + end + + test "it increases like count when receiving a like action", %{conn: conn} do + note_activity = insert(:note_activity) + user = User.get_cached_by_ap_id(note_activity.data["actor"]) + + data = %{ + type: "Like", + object: %{ + id: note_activity.data["object"]["id"] + } + } + + conn = + conn + |> assign(:user, user) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/outbox", data) + + result = json_response(conn, 201) + assert Activity.get_by_ap_id(result["id"]) + + object = Object.get_by_ap_id(note_activity.data["object"]["id"]) + assert object + assert object.data["like_count"] == 1 + end end describe "/users/:nickname/followers" do @@ -145,6 +499,20 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert result["first"]["orderedItems"] == [user.ap_id] end + test "it returns returns empty if the user has 'hide_followers' set", %{conn: conn} do + user = insert(:user) + user_two = insert(:user, %{info: %{hide_followers: true}}) + User.follow(user, user_two) + + result = + conn + |> get("/users/#{user_two.nickname}/followers") + |> json_response(200) + + assert result["first"]["orderedItems"] == [] + assert result["totalItems"] == 0 + end + test "it works for more than 10 users", %{conn: conn} do user = insert(:user) @@ -186,11 +554,25 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert result["first"]["orderedItems"] == [user_two.ap_id] end + test "it returns returns empty if the user has 'hide_follows' set", %{conn: conn} do + user = insert(:user, %{info: %{hide_follows: true}}) + user_two = insert(:user) + User.follow(user, user_two) + + result = + conn + |> get("/users/#{user.nickname}/following") + |> json_response(200) + + assert result["first"]["orderedItems"] == [] + assert result["totalItems"] == 0 + end + test "it works for more than 10 users", %{conn: conn} do user = insert(:user) Enum.each(1..15, fn _ -> - user = Repo.get(User, user.id) + user = User.get_by_id(user.id) other_user = insert(:user) User.follow(user, other_user) end) diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index bc9fcc75d..68bfb3858 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -1,12 +1,70 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.ActivityPub.ActivityPubTest do use Pleroma.DataCase + alias Pleroma.Activity + alias Pleroma.Builders.ActivityBuilder + alias Pleroma.Instances + alias Pleroma.Object + alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.CommonAPI - alias Pleroma.{Activity, Object, User} - alias Pleroma.Builders.ActivityBuilder import Pleroma.Factory + import Tesla.Mock + import Mock + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + describe "fetching restricted by visibility" do + test "it restricts by the appropriate visibility" do + user = insert(:user) + + {:ok, public_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"}) + + {:ok, direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"}) + + {:ok, unlisted_activity} = + CommonAPI.post(user, %{"status" => ".", "visibility" => "unlisted"}) + + {:ok, private_activity} = + CommonAPI.post(user, %{"status" => ".", "visibility" => "private"}) + + activities = + ActivityPub.fetch_activities([], %{:visibility => "direct", "actor_id" => user.ap_id}) + + assert activities == [direct_activity] + + activities = + ActivityPub.fetch_activities([], %{:visibility => "unlisted", "actor_id" => user.ap_id}) + + assert activities == [unlisted_activity] + + activities = + ActivityPub.fetch_activities([], %{:visibility => "private", "actor_id" => user.ap_id}) + + assert activities == [private_activity] + + activities = + ActivityPub.fetch_activities([], %{:visibility => "public", "actor_id" => user.ap_id}) + + assert activities == [public_activity] + + activities = + ActivityPub.fetch_activities([], %{ + :visibility => ~w[private public], + "actor_id" => user.ap_id + }) + + assert activities == [public_activity, private_activity] + end + end describe "building a user from his ap id" do test "it returns a user" do @@ -18,14 +76,71 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert user.info.ap_enabled assert user.follower_address == "http://mastodon.example.org/users/admin/followers" end + + test "it fetches the appropriate tag-restricted posts" do + user = insert(:user) + + {:ok, status_one} = CommonAPI.post(user, %{"status" => ". #test"}) + {:ok, status_two} = CommonAPI.post(user, %{"status" => ". #essais"}) + {:ok, status_three} = CommonAPI.post(user, %{"status" => ". #test #reject"}) + + fetch_one = ActivityPub.fetch_activities([], %{"tag" => "test"}) + fetch_two = ActivityPub.fetch_activities([], %{"tag" => ["test", "essais"]}) + + fetch_three = + ActivityPub.fetch_activities([], %{ + "tag" => ["test", "essais"], + "tag_reject" => ["reject"] + }) + + fetch_four = + ActivityPub.fetch_activities([], %{ + "tag" => ["test"], + "tag_all" => ["test", "reject"] + }) + + assert fetch_one == [status_one, status_three] + assert fetch_two == [status_one, status_two, status_three] + assert fetch_three == [status_one, status_two] + assert fetch_four == [status_three] + end end describe "insertion" do + test "drops activities beyond a certain limit" do + limit = Pleroma.Config.get([:instance, :remote_limit]) + + random_text = + :crypto.strong_rand_bytes(limit + 1) + |> Base.encode64() + |> binary_part(0, limit + 1) + + data = %{ + "ok" => true, + "object" => %{ + "content" => random_text + } + } + + assert {:error, {:remote_limit_error, _}} = ActivityPub.insert(data) + end + + test "doesn't drop activities with content being null" do + data = %{ + "ok" => true, + "object" => %{ + "content" => nil + } + } + + assert {:ok, _} = ActivityPub.insert(data) + end + test "returns the activity if one with the same id is already in" do activity = insert(:note_activity) {:ok, new_activity} = ActivityPub.insert(activity.data) - assert activity == new_activity + assert activity.id == new_activity.id end test "inserts a given map into the activity database, giving it an id if it has none." do @@ -102,7 +217,59 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert activity.data["to"] == ["user1", "user2"] assert activity.actor == user.ap_id - assert activity.recipients == ["user1", "user2"] + assert activity.recipients == ["user1", "user2", user.ap_id] + end + + test "increases user note count only for public activities" do + user = insert(:user) + + {:ok, _} = + CommonAPI.post(User.get_by_id(user.id), %{"status" => "1", "visibility" => "public"}) + + {:ok, _} = + CommonAPI.post(User.get_by_id(user.id), %{"status" => "2", "visibility" => "unlisted"}) + + {:ok, _} = + CommonAPI.post(User.get_by_id(user.id), %{"status" => "2", "visibility" => "private"}) + + {:ok, _} = + CommonAPI.post(User.get_by_id(user.id), %{"status" => "3", "visibility" => "direct"}) + + user = User.get_by_id(user.id) + assert user.info.note_count == 2 + end + + test "increases replies count" do + user = insert(:user) + user2 = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "1", "visibility" => "public"}) + ap_id = activity.data["id"] + reply_data = %{"status" => "1", "in_reply_to_status_id" => activity.id} + + # public + {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "public")) + assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) + assert data["object"]["repliesCount"] == 1 + assert object.data["repliesCount"] == 1 + + # unlisted + {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "unlisted")) + assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) + assert data["object"]["repliesCount"] == 2 + assert object.data["repliesCount"] == 2 + + # private + {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "private")) + assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) + assert data["object"]["repliesCount"] == 2 + assert object.data["repliesCount"] == 2 + + # direct + {:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "direct")) + assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) + assert data["object"]["repliesCount"] == 2 + assert object.data["repliesCount"] == 2 end end @@ -142,7 +309,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do booster = insert(:user) {:ok, user} = User.block(user, %{ap_id: activity_one.data["actor"]}) - activities = ActivityPub.fetch_activities([], %{"blocking_user" => user}) + activities = + ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true}) assert Enum.member?(activities, activity_two) assert Enum.member?(activities, activity_three) @@ -150,7 +318,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do {:ok, user} = User.unblock(user, %{ap_id: activity_one.data["actor"]}) - activities = ActivityPub.fetch_activities([], %{"blocking_user" => user}) + activities = + ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true}) assert Enum.member?(activities, activity_two) assert Enum.member?(activities, activity_three) @@ -158,17 +327,76 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do {:ok, user} = User.block(user, %{ap_id: activity_three.data["actor"]}) {:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(activity_three.id, booster) - %Activity{} = boost_activity = Activity.get_create_activity_by_object_ap_id(id) - activity_three = Repo.get(Activity, activity_three.id) + %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id) + activity_three = Activity.get_by_id(activity_three.id) + + activities = + ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true}) + + assert Enum.member?(activities, activity_two) + refute Enum.member?(activities, activity_three) + refute Enum.member?(activities, boost_activity) + assert Enum.member?(activities, activity_one) + + activities = + ActivityPub.fetch_activities([], %{"blocking_user" => nil, "skip_preload" => true}) + + assert Enum.member?(activities, activity_two) + assert Enum.member?(activities, activity_three) + assert Enum.member?(activities, boost_activity) + assert Enum.member?(activities, activity_one) + end + + test "doesn't return muted activities" do + activity_one = insert(:note_activity) + activity_two = insert(:note_activity) + activity_three = insert(:note_activity) + user = insert(:user) + booster = insert(:user) + {:ok, user} = User.mute(user, %User{ap_id: activity_one.data["actor"]}) + + activities = + ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true}) + + assert Enum.member?(activities, activity_two) + assert Enum.member?(activities, activity_three) + refute Enum.member?(activities, activity_one) + + # Calling with 'with_muted' will deliver muted activities, too. + activities = + ActivityPub.fetch_activities([], %{ + "muting_user" => user, + "with_muted" => true, + "skip_preload" => true + }) + + assert Enum.member?(activities, activity_two) + assert Enum.member?(activities, activity_three) + assert Enum.member?(activities, activity_one) + + {:ok, user} = User.unmute(user, %User{ap_id: activity_one.data["actor"]}) + + activities = + ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true}) + + assert Enum.member?(activities, activity_two) + assert Enum.member?(activities, activity_three) + assert Enum.member?(activities, activity_one) + + {:ok, user} = User.mute(user, %User{ap_id: activity_three.data["actor"]}) + {:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(activity_three.id, booster) + %Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id) + activity_three = Activity.get_by_id(activity_three.id) - activities = ActivityPub.fetch_activities([], %{"blocking_user" => user}) + activities = + ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true}) assert Enum.member?(activities, activity_two) refute Enum.member?(activities, activity_three) refute Enum.member?(activities, boost_activity) assert Enum.member?(activities, activity_one) - activities = ActivityPub.fetch_activities([], %{"blocking_user" => nil}) + activities = ActivityPub.fetch_activities([], %{"muting_user" => nil, "skip_preload" => true}) assert Enum.member?(activities, activity_two) assert Enum.member?(activities, activity_three) @@ -176,11 +404,35 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert Enum.member?(activities, activity_one) end + test "does include announces on request" do + activity_three = insert(:note_activity) + user = insert(:user) + booster = insert(:user) + + {:ok, user} = User.follow(user, booster) + + {:ok, announce, _object} = CommonAPI.repeat(activity_three.id, booster) + + [announce_activity] = ActivityPub.fetch_activities([user.ap_id | user.following]) + + assert announce_activity.id == announce.id + end + + test "excludes reblogs on request" do + user = insert(:user) + {:ok, expected_activity} = ActivityBuilder.insert(%{"type" => "Create"}, %{:user => user}) + {:ok, _} = ActivityBuilder.insert(%{"type" => "Announce"}, %{:user => user}) + + [activity] = ActivityPub.fetch_user_activities(user, nil, %{"exclude_reblogs" => "true"}) + + assert activity == expected_activity + end + describe "public fetch activities" do test "doesn't retrieve unlisted activities" do user = insert(:user) - {:ok, unlisted_activity} = + {:ok, _unlisted_activity} = CommonAPI.post(user, %{"status" => "yeah", "visibility" => "unlisted"}) {:ok, listed_activity} = CommonAPI.post(user, %{"status" => "yeah"}) @@ -237,6 +489,33 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert length(activities) == 20 assert last == last_expected end + + test "doesn't return reblogs for users for whom reblogs have been muted" do + activity = insert(:note_activity) + user = insert(:user) + booster = insert(:user) + {:ok, user} = CommonAPI.hide_reblogs(user, booster) + + {:ok, activity, _} = CommonAPI.repeat(activity.id, booster) + + activities = ActivityPub.fetch_activities([], %{"muting_user" => user}) + + refute Enum.any?(activities, fn %{id: id} -> id == activity.id end) + end + + test "returns reblogs for users for whom reblogs have not been muted" do + activity = insert(:note_activity) + user = insert(:user) + booster = insert(:user) + {:ok, user} = CommonAPI.hide_reblogs(user, booster) + {:ok, user} = CommonAPI.show_reblogs(user, booster) + + {:ok, activity, _} = CommonAPI.repeat(activity.id, booster) + + activities = ActivityPub.fetch_activities([], %{"muting_user" => user}) + + assert Enum.any?(activities, fn %{id: id} -> id == activity.id end) + end end describe "like an object" do @@ -262,7 +541,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert like_activity == same_like_activity assert object.data["likes"] == [user.ap_id] - [note_activity] = Activity.all_by_object_ap_id(object.data["id"]) + [note_activity] = Activity.get_all_create_by_object_ap_id(object.data["id"]) assert note_activity.data["object"]["like_count"] == 1 {:ok, _like_activity, object} = ActivityPub.like(user_two, object) @@ -286,7 +565,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do {:ok, _, _, object} = ActivityPub.unlike(user, object) assert object.data["like_count"] == 0 - assert Repo.get(Activity, like_activity.id) == nil + assert Activity.get_by_id(like_activity.id) == nil end end @@ -337,7 +616,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert unannounce_activity.data["actor"] == user.ap_id assert unannounce_activity.data["context"] == announce_activity.data["context"] - assert Repo.get(Activity, announce_activity.id) == nil + assert Activity.get_by_id(announce_activity.id) == nil end end @@ -372,6 +651,43 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do end end + describe "fetching an object" do + test "it fetches an object" do + {:ok, object} = + ActivityPub.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") + + assert activity = Activity.get_create_by_object_ap_id(object.data["id"]) + assert activity.data["id"] + + {:ok, object_again} = + ActivityPub.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") + + assert [attachment] = object.data["attachment"] + assert is_list(attachment["url"]) + + assert object == object_again + end + + test "it works with objects only available via Ostatus" do + {:ok, object} = ActivityPub.fetch_object_from_id("https://shitposter.club/notice/2827873") + assert activity = Activity.get_create_by_object_ap_id(object.data["id"]) + assert activity.data["id"] + + {:ok, object_again} = + ActivityPub.fetch_object_from_id("https://shitposter.club/notice/2827873") + + assert object == object_again + end + + test "it correctly stitches up conversations between ostatus and ap" do + last = "https://mstdn.io/users/mayuutann/statuses/99568293732299394" + {:ok, object} = ActivityPub.fetch_object_from_id(last) + + object = Object.get_by_ap_id(object.data["inReplyTo"]) + assert object + end + end + describe "following / unfollowing" do test "creates a follow activity" do follower = insert(:user) @@ -439,9 +755,88 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert delete.data["actor"] == note.data["actor"] assert delete.data["object"] == note.data["object"]["id"] - assert Repo.get(Activity, delete.id) != nil + assert Activity.get_by_id(delete.id) != nil - assert Repo.get(Object, object.id) == nil + assert Repo.get(Object, object.id).data["type"] == "Tombstone" + end + + test "decrements user note count only for public activities" do + user = insert(:user, info: %{note_count: 10}) + + {:ok, a1} = + CommonAPI.post(User.get_by_id(user.id), %{"status" => "yeah", "visibility" => "public"}) + + {:ok, a2} = + CommonAPI.post(User.get_by_id(user.id), %{"status" => "yeah", "visibility" => "unlisted"}) + + {:ok, a3} = + CommonAPI.post(User.get_by_id(user.id), %{"status" => "yeah", "visibility" => "private"}) + + {:ok, a4} = + CommonAPI.post(User.get_by_id(user.id), %{"status" => "yeah", "visibility" => "direct"}) + + {:ok, _} = a1.data["object"]["id"] |> Object.get_by_ap_id() |> ActivityPub.delete() + {:ok, _} = a2.data["object"]["id"] |> Object.get_by_ap_id() |> ActivityPub.delete() + {:ok, _} = a3.data["object"]["id"] |> Object.get_by_ap_id() |> ActivityPub.delete() + {:ok, _} = a4.data["object"]["id"] |> Object.get_by_ap_id() |> ActivityPub.delete() + + user = User.get_by_id(user.id) + assert user.info.note_count == 10 + end + + test "it creates a delete activity and checks that it is also sent to users mentioned by the deleted object" do + user = insert(:user) + note = insert(:note_activity) + + {:ok, object} = + Object.get_by_ap_id(note.data["object"]["id"]) + |> Object.change(%{ + data: %{ + "actor" => note.data["object"]["actor"], + "id" => note.data["object"]["id"], + "to" => [user.ap_id], + "type" => "Note" + } + }) + |> Object.update_and_set_cache() + + {:ok, delete} = ActivityPub.delete(object) + + assert user.ap_id in delete.data["to"] + end + + test "decreases reply count" do + user = insert(:user) + user2 = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "1", "visibility" => "public"}) + reply_data = %{"status" => "1", "in_reply_to_status_id" => activity.id} + ap_id = activity.data["id"] + + {:ok, public_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "public")) + {:ok, unlisted_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "unlisted")) + {:ok, private_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "private")) + {:ok, direct_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "direct")) + + _ = CommonAPI.delete(direct_reply.id, user2) + assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) + assert data["object"]["repliesCount"] == 2 + assert object.data["repliesCount"] == 2 + + _ = CommonAPI.delete(private_reply.id, user2) + assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) + assert data["object"]["repliesCount"] == 2 + assert object.data["repliesCount"] == 2 + + _ = CommonAPI.delete(public_reply.id, user2) + assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) + assert data["object"]["repliesCount"] == 1 + assert object.data["repliesCount"] == 1 + + _ = CommonAPI.delete(unlisted_reply.id, user2) + assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) + assert data["object"]["repliesCount"] == 0 + assert object.data["repliesCount"] == 0 end end @@ -479,10 +874,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do "in_reply_to_status_id" => private_activity_2.id }) - assert user1.following == [user3.ap_id <> "/followers", user1.ap_id] - activities = ActivityPub.fetch_activities([user1.ap_id | user1.following]) + private_activity_1 = Activity.get_by_ap_id_with_object(private_activity_1.data["id"]) assert [public_activity, private_activity_1, private_activity_3] == activities assert length(activities) == 3 @@ -514,6 +908,177 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do end end + test "it can fetch peertube videos" do + {:ok, object} = + ActivityPub.fetch_object_from_id( + "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" + ) + + assert object + end + + test "returned pinned statuses" do + Pleroma.Config.put([:instance, :max_pinned_statuses], 3) + user = insert(:user) + + {:ok, activity_one} = CommonAPI.post(user, %{"status" => "HI!!!"}) + {:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"}) + {:ok, activity_three} = CommonAPI.post(user, %{"status" => "HI!!!"}) + + CommonAPI.pin(activity_one.id, user) + user = refresh_record(user) + + CommonAPI.pin(activity_two.id, user) + user = refresh_record(user) + + CommonAPI.pin(activity_three.id, user) + user = refresh_record(user) + + activities = ActivityPub.fetch_user_activities(user, nil, %{"pinned" => "true"}) + + assert 3 = length(activities) + end + + test "it can create a Flag activity" do + reporter = insert(:user) + target_account = insert(:user) + {:ok, activity} = CommonAPI.post(target_account, %{"status" => "foobar"}) + context = Utils.generate_context_id() + content = "foobar" + + reporter_ap_id = reporter.ap_id + target_ap_id = target_account.ap_id + activity_ap_id = activity.data["id"] + + assert {:ok, activity} = + ActivityPub.flag(%{ + actor: reporter, + context: context, + account: target_account, + statuses: [activity], + content: content + }) + + assert %Activity{ + actor: ^reporter_ap_id, + data: %{ + "type" => "Flag", + "content" => ^content, + "context" => ^context, + "object" => [^target_ap_id, ^activity_ap_id] + } + } = activity + end + + describe "publish_one/1" do + test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is not specified", + Instances, + [:passthrough], + [] do + actor = insert(:user) + inbox = "http://200.site/users/nick1/inbox" + + assert {:ok, _} = ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) + + assert called(Instances.set_reachable(inbox)) + end + + test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is set", + Instances, + [:passthrough], + [] do + actor = insert(:user) + inbox = "http://200.site/users/nick1/inbox" + + assert {:ok, _} = + ActivityPub.publish_one(%{ + inbox: inbox, + json: "{}", + actor: actor, + id: 1, + unreachable_since: NaiveDateTime.utc_now() + }) + + assert called(Instances.set_reachable(inbox)) + end + + test_with_mock "does NOT call `Instances.set_reachable` on successful federation if `unreachable_since` is nil", + Instances, + [:passthrough], + [] do + actor = insert(:user) + inbox = "http://200.site/users/nick1/inbox" + + assert {:ok, _} = + ActivityPub.publish_one(%{ + inbox: inbox, + json: "{}", + actor: actor, + id: 1, + unreachable_since: nil + }) + + refute called(Instances.set_reachable(inbox)) + end + + test_with_mock "calls `Instances.set_unreachable` on target inbox on non-2xx HTTP response code", + Instances, + [:passthrough], + [] do + actor = insert(:user) + inbox = "http://404.site/users/nick1/inbox" + + assert {:error, _} = + ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) + + assert called(Instances.set_unreachable(inbox)) + end + + test_with_mock "it calls `Instances.set_unreachable` on target inbox on request error of any kind", + Instances, + [:passthrough], + [] do + actor = insert(:user) + inbox = "http://connrefused.site/users/nick1/inbox" + + assert {:error, _} = + ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) + + assert called(Instances.set_unreachable(inbox)) + end + + test_with_mock "does NOT call `Instances.set_unreachable` if target is reachable", + Instances, + [:passthrough], + [] do + actor = insert(:user) + inbox = "http://200.site/users/nick1/inbox" + + assert {:ok, _} = ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) + + refute called(Instances.set_unreachable(inbox)) + end + + test_with_mock "does NOT call `Instances.set_unreachable` if target instance has non-nil `unreachable_since`", + Instances, + [:passthrough], + [] do + actor = insert(:user) + inbox = "http://connrefused.site/users/nick1/inbox" + + assert {:error, _} = + ActivityPub.publish_one(%{ + inbox: inbox, + json: "{}", + actor: actor, + id: 1, + unreachable_since: NaiveDateTime.utc_now() + }) + + refute called(Instances.set_unreachable(inbox)) + end + end + def data_uri do File.read!("test/fixtures/avatar_data_uri") end diff --git a/test/web/activity_pub/mrf/anti_followbot_policy_test.exs b/test/web/activity_pub/mrf/anti_followbot_policy_test.exs new file mode 100644 index 000000000..37a7bfcf7 --- /dev/null +++ b/test/web/activity_pub/mrf/anti_followbot_policy_test.exs @@ -0,0 +1,72 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicyTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy + + describe "blocking based on attributes" do + test "matches followbots by nickname" do + actor = insert(:user, %{nickname: "followbot@example.com"}) + target = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Follow", + "actor" => actor.ap_id, + "object" => target.ap_id, + "id" => "https://example.com/activities/1234" + } + + {:reject, nil} = AntiFollowbotPolicy.filter(message) + end + + test "matches followbots by display name" do + actor = insert(:user, %{name: "Federation Bot"}) + target = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Follow", + "actor" => actor.ap_id, + "object" => target.ap_id, + "id" => "https://example.com/activities/1234" + } + + {:reject, nil} = AntiFollowbotPolicy.filter(message) + end + end + + test "it allows non-followbots" do + actor = insert(:user) + target = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Follow", + "actor" => actor.ap_id, + "object" => target.ap_id, + "id" => "https://example.com/activities/1234" + } + + {:ok, _} = AntiFollowbotPolicy.filter(message) + end + + test "it gracefully handles nil display names" do + actor = insert(:user, %{name: nil}) + target = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Follow", + "actor" => actor.ap_id, + "object" => target.ap_id, + "id" => "https://example.com/activities/1234" + } + + {:ok, _} = AntiFollowbotPolicy.filter(message) + end +end diff --git a/test/web/activity_pub/mrf/hellthread_policy_test.exs b/test/web/activity_pub/mrf/hellthread_policy_test.exs new file mode 100644 index 000000000..eb6ee4d04 --- /dev/null +++ b/test/web/activity_pub/mrf/hellthread_policy_test.exs @@ -0,0 +1,73 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicyTest do + use Pleroma.DataCase + import Pleroma.Factory + + import Pleroma.Web.ActivityPub.MRF.HellthreadPolicy + + setup do + user = insert(:user) + + message = %{ + "actor" => user.ap_id, + "cc" => [user.follower_address], + "type" => "Create", + "to" => [ + "https://www.w3.org/ns/activitystreams#Public", + "https://instance.tld/users/user1", + "https://instance.tld/users/user2", + "https://instance.tld/users/user3" + ] + } + + [user: user, message: message] + end + + describe "reject" do + test "rejects the message if the recipient count is above reject_threshold", %{ + message: message + } do + Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 0, reject_threshold: 2}) + + {:reject, nil} = filter(message) + end + + test "does not reject the message if the recipient count is below reject_threshold", %{ + message: message + } do + Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 0, reject_threshold: 3}) + + assert {:ok, ^message} = filter(message) + end + end + + describe "delist" do + test "delists the message if the recipient count is above delist_threshold", %{ + user: user, + message: message + } do + Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 2, reject_threshold: 0}) + + {:ok, message} = filter(message) + assert user.follower_address in message["to"] + assert "https://www.w3.org/ns/activitystreams#Public" in message["cc"] + end + + test "does not delist the message if the recipient count is below delist_threshold", %{ + message: message + } do + Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 4, reject_threshold: 0}) + + assert {:ok, ^message} = filter(message) + end + end + + test "excludes follower collection and public URI from threshold count", %{message: message} do + Pleroma.Config.put([:mrf_hellthread], %{delist_threshold: 0, reject_threshold: 3}) + + assert {:ok, ^message} = filter(message) + end +end diff --git a/test/web/activity_pub/mrf/keyword_policy_test.exs b/test/web/activity_pub/mrf/keyword_policy_test.exs new file mode 100644 index 000000000..602892a37 --- /dev/null +++ b/test/web/activity_pub/mrf/keyword_policy_test.exs @@ -0,0 +1,219 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicyTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.MRF.KeywordPolicy + + setup do + Pleroma.Config.put([:mrf_keyword], %{reject: [], federated_timeline_removal: [], replace: []}) + end + + describe "rejecting based on keywords" do + test "rejects if string matches in content" do + Pleroma.Config.put([:mrf_keyword, :reject], ["pun"]) + + message = %{ + "type" => "Create", + "object" => %{ + "content" => "just a daily reminder that compLAINer is a good pun", + "summary" => "" + } + } + + assert {:reject, nil} == KeywordPolicy.filter(message) + end + + test "rejects if string matches in summary" do + Pleroma.Config.put([:mrf_keyword, :reject], ["pun"]) + + message = %{ + "type" => "Create", + "object" => %{ + "summary" => "just a daily reminder that compLAINer is a good pun", + "content" => "" + } + } + + assert {:reject, nil} == KeywordPolicy.filter(message) + end + + test "rejects if regex matches in content" do + Pleroma.Config.put([:mrf_keyword, :reject], [~r/comp[lL][aA][iI][nN]er/]) + + assert true == + Enum.all?(["complainer", "compLainer", "compLAiNer", "compLAINer"], fn content -> + message = %{ + "type" => "Create", + "object" => %{ + "content" => "just a daily reminder that #{content} is a good pun", + "summary" => "" + } + } + + {:reject, nil} == KeywordPolicy.filter(message) + end) + end + + test "rejects if regex matches in summary" do + Pleroma.Config.put([:mrf_keyword, :reject], [~r/comp[lL][aA][iI][nN]er/]) + + assert true == + Enum.all?(["complainer", "compLainer", "compLAiNer", "compLAINer"], fn content -> + message = %{ + "type" => "Create", + "object" => %{ + "summary" => "just a daily reminder that #{content} is a good pun", + "content" => "" + } + } + + {:reject, nil} == KeywordPolicy.filter(message) + end) + end + end + + describe "delisting from ftl based on keywords" do + test "delists if string matches in content" do + Pleroma.Config.put([:mrf_keyword, :federated_timeline_removal], ["pun"]) + + message = %{ + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "type" => "Create", + "object" => %{ + "content" => "just a daily reminder that compLAINer is a good pun", + "summary" => "" + } + } + + {:ok, result} = KeywordPolicy.filter(message) + assert ["https://www.w3.org/ns/activitystreams#Public"] == result["cc"] + refute ["https://www.w3.org/ns/activitystreams#Public"] == result["to"] + end + + test "delists if string matches in summary" do + Pleroma.Config.put([:mrf_keyword, :federated_timeline_removal], ["pun"]) + + message = %{ + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "type" => "Create", + "object" => %{ + "summary" => "just a daily reminder that compLAINer is a good pun", + "content" => "" + } + } + + {:ok, result} = KeywordPolicy.filter(message) + assert ["https://www.w3.org/ns/activitystreams#Public"] == result["cc"] + refute ["https://www.w3.org/ns/activitystreams#Public"] == result["to"] + end + + test "delists if regex matches in content" do + Pleroma.Config.put([:mrf_keyword, :federated_timeline_removal], [~r/comp[lL][aA][iI][nN]er/]) + + assert true == + Enum.all?(["complainer", "compLainer", "compLAiNer", "compLAINer"], fn content -> + message = %{ + "type" => "Create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "object" => %{ + "content" => "just a daily reminder that #{content} is a good pun", + "summary" => "" + } + } + + {:ok, result} = KeywordPolicy.filter(message) + + ["https://www.w3.org/ns/activitystreams#Public"] == result["cc"] and + not (["https://www.w3.org/ns/activitystreams#Public"] == result["to"]) + end) + end + + test "delists if regex matches in summary" do + Pleroma.Config.put([:mrf_keyword, :federated_timeline_removal], [~r/comp[lL][aA][iI][nN]er/]) + + assert true == + Enum.all?(["complainer", "compLainer", "compLAiNer", "compLAINer"], fn content -> + message = %{ + "type" => "Create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "object" => %{ + "summary" => "just a daily reminder that #{content} is a good pun", + "content" => "" + } + } + + {:ok, result} = KeywordPolicy.filter(message) + + ["https://www.w3.org/ns/activitystreams#Public"] == result["cc"] and + not (["https://www.w3.org/ns/activitystreams#Public"] == result["to"]) + end) + end + end + + describe "replacing keywords" do + test "replaces keyword if string matches in content" do + Pleroma.Config.put([:mrf_keyword, :replace], [{"opensource", "free software"}]) + + message = %{ + "type" => "Create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "object" => %{"content" => "ZFS is opensource", "summary" => ""} + } + + {:ok, %{"object" => %{"content" => result}}} = KeywordPolicy.filter(message) + assert result == "ZFS is free software" + end + + test "replaces keyword if string matches in summary" do + Pleroma.Config.put([:mrf_keyword, :replace], [{"opensource", "free software"}]) + + message = %{ + "type" => "Create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "object" => %{"summary" => "ZFS is opensource", "content" => ""} + } + + {:ok, %{"object" => %{"summary" => result}}} = KeywordPolicy.filter(message) + assert result == "ZFS is free software" + end + + test "replaces keyword if regex matches in content" do + Pleroma.Config.put([:mrf_keyword, :replace], [ + {~r/open(-|\s)?source\s?(software)?/, "free software"} + ]) + + assert true == + Enum.all?(["opensource", "open-source", "open source"], fn content -> + message = %{ + "type" => "Create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "object" => %{"content" => "ZFS is #{content}", "summary" => ""} + } + + {:ok, %{"object" => %{"content" => result}}} = KeywordPolicy.filter(message) + result == "ZFS is free software" + end) + end + + test "replaces keyword if regex matches in summary" do + Pleroma.Config.put([:mrf_keyword, :replace], [ + {~r/open(-|\s)?source\s?(software)?/, "free software"} + ]) + + assert true == + Enum.all?(["opensource", "open-source", "open source"], fn content -> + message = %{ + "type" => "Create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "object" => %{"summary" => "ZFS is #{content}", "content" => ""} + } + + {:ok, %{"object" => %{"summary" => result}}} = KeywordPolicy.filter(message) + result == "ZFS is free software" + end) + end + end +end diff --git a/test/web/activity_pub/relay_test.exs b/test/web/activity_pub/relay_test.exs index 41d13e055..21a63c493 100644 --- a/test/web/activity_pub/relay_test.exs +++ b/test/web/activity_pub/relay_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.ActivityPub.RelayTest do use Pleroma.DataCase diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index ea9d9fe58..5559cdf87 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -1,17 +1,27 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do use Pleroma.DataCase + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Utils - alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.OStatus - alias Pleroma.{Activity, Object} - alias Pleroma.User - alias Pleroma.Repo alias Pleroma.Web.Websub.WebsubClientSubscription import Pleroma.Factory alias Pleroma.Web.CommonAPI + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + describe "handle_incoming" do test "it ignores an incoming notice if we already have it" do activity = insert(:note_activity) @@ -43,13 +53,11 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do returned_object = Object.normalize(returned_activity.data["object"]) assert activity = - Activity.get_create_activity_by_object_ap_id( + Activity.get_create_by_object_ap_id( "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment" ) assert returned_object.data["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873" - - assert returned_object.data["inReplyToStatusId"] == activity.id end test "it works for incoming notices" do @@ -160,6 +168,36 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do assert object.data["url"] == "https://prismo.news/posts/83" end + test "it cleans up incoming notices which are not really DMs" do + user = insert(:user) + other_user = insert(:user) + + to = [user.ap_id, other_user.ap_id] + + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + |> Map.put("to", to) + |> Map.put("cc", []) + + object = + data["object"] + |> Map.put("to", to) + |> Map.put("cc", []) + + data = Map.put(data, "object", object) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["to"] == [] + assert data["cc"] == to + + object = data["object"] + + assert object["to"] == [] + assert object["cc"] == to + end + test "it works for incoming follow requests" do user = insert(:user) @@ -261,7 +299,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do assert data["object"] == "http://mastodon.example.org/users/admin/statuses/99541947525187367" - assert Activity.get_create_activity_by_object_ap_id(data["object"]) + assert Activity.get_create_by_object_ap_id(data["object"]) end test "it works for incoming announces with an existing activity" do @@ -283,7 +321,70 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do assert data["object"] == activity.data["object"] - assert Activity.get_create_activity_by_object_ap_id(data["object"]).id == activity.id + assert Activity.get_create_by_object_ap_id(data["object"]).id == activity.id + end + + test "it does not clobber the addressing on announce activities" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{"status" => "hey"}) + + data = + File.read!("test/fixtures/mastodon-announce.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]["id"]) + |> Map.put("to", ["http://mastodon.example.org/users/admin/followers"]) + |> Map.put("cc", []) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["to"] == ["http://mastodon.example.org/users/admin/followers"] + end + + test "it ensures that as:Public activities make it to their followers collection" do + user = insert(:user) + + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + |> Map.put("actor", user.ap_id) + |> Map.put("to", ["https://www.w3.org/ns/activitystreams#Public"]) + |> Map.put("cc", []) + + object = + data["object"] + |> Map.put("attributedTo", user.ap_id) + |> Map.put("to", ["https://www.w3.org/ns/activitystreams#Public"]) + |> Map.put("cc", []) + + data = Map.put(data, "object", object) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["cc"] == [User.ap_followers(user)] + end + + test "it ensures that address fields become lists" do + user = insert(:user) + + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + |> Map.put("actor", user.ap_id) + |> Map.put("to", nil) + |> Map.put("cc", nil) + + object = + data["object"] + |> Map.put("attributedTo", user.ap_id) + |> Map.put("to", nil) + |> Map.put("cc", nil) + + data = Map.put(data, "object", object) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert !is_nil(data["to"]) + assert !is_nil(data["cc"]) end test "it works for incoming update activities" do @@ -365,7 +466,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data) - refute Repo.get(Activity, activity.id) + refute Activity.get_by_id(activity.id) end test "it fails for incoming deletes with spoofed origin" do @@ -385,7 +486,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do :error = Transmogrifier.handle_incoming(data) - assert Repo.get(Activity, activity.id) + assert Activity.get_by_id(activity.id) end test "it works for incoming unannounces with an existing notice" do @@ -543,7 +644,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do assert activity.data["object"] == follow_activity.data["id"] - follower = Repo.get(User, follower.id) + follower = User.get_by_id(follower.id) assert User.following?(follower, followed) == true end @@ -565,7 +666,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do {:ok, activity} = Transmogrifier.handle_incoming(accept_data) assert activity.data["object"] == follow_activity.data["id"] - follower = Repo.get(User, follower.id) + follower = User.get_by_id(follower.id) assert User.following?(follower, followed) == true end @@ -585,7 +686,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do {:ok, activity} = Transmogrifier.handle_incoming(accept_data) assert activity.data["object"] == follow_activity.data["id"] - follower = Repo.get(User, follower.id) + follower = User.get_by_id(follower.id) assert User.following?(follower, followed) == true end @@ -604,7 +705,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do :error = Transmogrifier.handle_incoming(accept_data) - follower = Repo.get(User, follower.id) + follower = User.get_by_id(follower.id) refute User.following?(follower, followed) == true end @@ -623,7 +724,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do :error = Transmogrifier.handle_incoming(accept_data) - follower = Repo.get(User, follower.id) + follower = User.get_by_id(follower.id) refute User.following?(follower, followed) == true end @@ -648,7 +749,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do {:ok, activity} = Transmogrifier.handle_incoming(reject_data) refute activity.local - follower = Repo.get(User, follower.id) + follower = User.get_by_id(follower.id) assert User.following?(follower, followed) == false end @@ -670,7 +771,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do {:ok, %Activity{data: _}} = Transmogrifier.handle_incoming(reject_data) - follower = Repo.get(User, follower.id) + follower = User.get_by_id(follower.id) assert User.following?(follower, followed) == false end @@ -686,6 +787,60 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do :error = Transmogrifier.handle_incoming(data) end + + test "it remaps video URLs as attachments if necessary" do + {:ok, object} = + ActivityPub.fetch_object_from_id( + "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" + ) + + attachment = %{ + "type" => "Link", + "mediaType" => "video/mp4", + "href" => + "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4", + "mimeType" => "video/mp4", + "size" => 5_015_880, + "url" => [ + %{ + "href" => + "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4", + "mediaType" => "video/mp4", + "type" => "Link" + } + ], + "width" => 480 + } + + assert object.data["url"] == + "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" + + assert object.data["attachment"] == [attachment] + end + + test "it accepts Flag activities" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "test post"}) + object = Object.normalize(activity.data["object"]) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "cc" => [user.ap_id], + "object" => [user.ap_id, object.data["id"]], + "type" => "Flag", + "content" => "blocked AND reported!!!", + "actor" => other_user.ap_id + } + + assert {:ok, activity} = Transmogrifier.handle_incoming(message) + + assert activity.data["object"] == [user.ap_id, object.data["id"]] + assert activity.data["content"] == "blocked AND reported!!!" + assert activity.data["actor"] == other_user.ap_id + assert activity.data["cc"] == [user.ap_id] + end end describe "prepare outgoing" do @@ -797,12 +952,61 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do assert length(modified["object"]["tag"]) == 2 assert is_nil(modified["object"]["emoji"]) - assert is_nil(modified["object"]["likes"]) assert is_nil(modified["object"]["like_count"]) assert is_nil(modified["object"]["announcements"]) assert is_nil(modified["object"]["announcement_count"]) assert is_nil(modified["object"]["context_id"]) end + + test "it strips internal fields of article" do + activity = insert(:article_activity) + + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + assert length(modified["object"]["tag"]) == 2 + + assert is_nil(modified["object"]["emoji"]) + assert is_nil(modified["object"]["like_count"]) + assert is_nil(modified["object"]["announcements"]) + assert is_nil(modified["object"]["announcement_count"]) + assert is_nil(modified["object"]["context_id"]) + end + + test "it adds like collection to object" do + activity = insert(:note_activity) + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + assert modified["object"]["likes"]["type"] == "OrderedCollection" + assert modified["object"]["likes"]["totalItems"] == 0 + end + + test "the directMessage flag is present" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "2hu :moominmamma:"}) + + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + assert modified["directMessage"] == false + + {:ok, activity} = + CommonAPI.post(user, %{"status" => "@#{other_user.nickname} :moominmamma:"}) + + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + assert modified["directMessage"] == false + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "@#{other_user.nickname} :moominmamma:", + "visibility" => "direct" + }) + + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + assert modified["directMessage"] == true + end end describe "user upgrade" do @@ -821,7 +1025,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do {:ok, unrelated_activity} = CommonAPI.post(user_two, %{"status" => "test"}) assert "http://localhost:4001/users/rye@niu.moe/followers" in activity.recipients - user = Repo.get(User, user.id) + user = User.get_by_id(user.id) assert user.info.note_count == 1 {:ok, user} = Transmogrifier.upgrade_user_from_ap_id("https://niu.moe/users/rye") @@ -829,13 +1033,10 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do assert user.info.note_count == 1 assert user.follower_address == "https://niu.moe/users/rye/followers" - # Wait for the background task - :timer.sleep(1000) - - user = Repo.get(User, user.id) + user = User.get_by_id(user.id) assert user.info.note_count == 1 - activity = Repo.get(Activity, activity.id) + activity = Activity.get_by_id(activity.id) assert user.follower_address in activity.recipients assert %{ @@ -858,10 +1059,10 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do refute "..." in activity.recipients - unrelated_activity = Repo.get(Activity, unrelated_activity.id) + unrelated_activity = Activity.get_by_id(unrelated_activity.id) refute user.follower_address in unrelated_activity.recipients - user_two = Repo.get(User, user_two.id) + user_two = User.get_by_id(user_two.id) assert user.follower_address in user_two.following refute "..." in user_two.following end @@ -933,4 +1134,114 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do :error = Transmogrifier.handle_incoming(data) end end + + describe "general origin containment" do + test "contain_origin_from_id() catches obvious spoofing attempts" do + data = %{ + "id" => "http://example.com/~alyssa/activities/1234.json" + } + + :error = + Transmogrifier.contain_origin_from_id( + "http://example.org/~alyssa/activities/1234.json", + data + ) + end + + test "contain_origin_from_id() allows alternate IDs within the same origin domain" do + data = %{ + "id" => "http://example.com/~alyssa/activities/1234.json" + } + + :ok = + Transmogrifier.contain_origin_from_id( + "http://example.com/~alyssa/activities/1234", + data + ) + end + + test "contain_origin_from_id() allows matching IDs" do + data = %{ + "id" => "http://example.com/~alyssa/activities/1234.json" + } + + :ok = + Transmogrifier.contain_origin_from_id( + "http://example.com/~alyssa/activities/1234.json", + data + ) + end + + test "users cannot be collided through fake direction spoofing attempts" do + insert(:user, %{ + nickname: "rye@niu.moe", + local: false, + ap_id: "https://niu.moe/users/rye", + follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"}) + }) + + {:error, _} = User.get_or_fetch_by_ap_id("https://n1u.moe/users/rye") + end + + test "all objects with fake directions are rejected by the object fetcher" do + {:error, _} = + ActivityPub.fetch_and_contain_remote_object_from_id( + "https://info.pleroma.site/activity4.json" + ) + end + end + + describe "reserialization" do + test "successfully reserializes a message with inReplyTo == nil" do + user = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "type" => "Create", + "object" => %{ + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "type" => "Note", + "content" => "Hi", + "inReplyTo" => nil, + "attributedTo" => user.ap_id + }, + "actor" => user.ap_id + } + + {:ok, activity} = Transmogrifier.handle_incoming(message) + + {:ok, _} = Transmogrifier.prepare_outgoing(activity.data) + end + + test "successfully reserializes a message with AS2 objects in IR" do + user = insert(:user) + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "type" => "Create", + "object" => %{ + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [], + "type" => "Note", + "content" => "Hi", + "inReplyTo" => nil, + "attributedTo" => user.ap_id, + "tag" => [ + %{"name" => "#2hu", "href" => "http://example.com/2hu", "type" => "Hashtag"}, + %{"name" => "Bob", "href" => "http://example.com/bob", "type" => "Mention"} + ] + }, + "actor" => user.ap_id + } + + {:ok, activity} = Transmogrifier.handle_incoming(message) + + {:ok, _} = Transmogrifier.prepare_outgoing(activity.data) + end + end end diff --git a/test/web/activity_pub/utils_test.exs b/test/web/activity_pub/utils_test.exs new file mode 100644 index 000000000..758214e68 --- /dev/null +++ b/test/web/activity_pub/utils_test.exs @@ -0,0 +1,208 @@ +defmodule Pleroma.Web.ActivityPub.UtilsTest do + use Pleroma.DataCase + alias Pleroma.Activity + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "fetch the latest Follow" do + test "fetches the latest Follow activity" do + %Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity) + follower = Repo.get_by(User, ap_id: activity.data["actor"]) + followed = Repo.get_by(User, ap_id: activity.data["object"]) + + assert activity == Utils.fetch_latest_follow(follower, followed) + end + end + + describe "fetch the latest Block" do + test "fetches the latest Block activity" do + blocker = insert(:user) + blocked = insert(:user) + {:ok, activity} = ActivityPub.block(blocker, blocked) + + assert activity == Utils.fetch_latest_block(blocker, blocked) + end + end + + describe "determine_explicit_mentions()" do + test "works with an object that has mentions" do + object = %{ + "tag" => [ + %{ + "type" => "Mention", + "href" => "https://example.com/~alyssa", + "name" => "Alyssa P. Hacker" + } + ] + } + + assert Utils.determine_explicit_mentions(object) == ["https://example.com/~alyssa"] + end + + test "works with an object that does not have mentions" do + object = %{ + "tag" => [ + %{"type" => "Hashtag", "href" => "https://example.com/tag/2hu", "name" => "2hu"} + ] + } + + assert Utils.determine_explicit_mentions(object) == [] + end + + test "works with an object that has mentions and other tags" do + object = %{ + "tag" => [ + %{ + "type" => "Mention", + "href" => "https://example.com/~alyssa", + "name" => "Alyssa P. Hacker" + }, + %{"type" => "Hashtag", "href" => "https://example.com/tag/2hu", "name" => "2hu"} + ] + } + + assert Utils.determine_explicit_mentions(object) == ["https://example.com/~alyssa"] + end + + test "works with an object that has no tags" do + object = %{} + + assert Utils.determine_explicit_mentions(object) == [] + end + + test "works with an object that has only IR tags" do + object = %{"tag" => ["2hu"]} + + assert Utils.determine_explicit_mentions(object) == [] + end + end + + describe "make_like_data" do + setup do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + [user: user, other_user: other_user, third_user: third_user] + end + + test "addresses actor's follower address if the activity is public", %{ + user: user, + other_user: other_user, + third_user: third_user + } do + expected_to = Enum.sort([user.ap_id, other_user.follower_address]) + expected_cc = Enum.sort(["https://www.w3.org/ns/activitystreams#Public", third_user.ap_id]) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => + "hey @#{other_user.nickname}, @#{third_user.nickname} how about beering together this weekend?" + }) + + %{"to" => to, "cc" => cc} = Utils.make_like_data(other_user, activity, nil) + assert Enum.sort(to) == expected_to + assert Enum.sort(cc) == expected_cc + end + + test "does not adress actor's follower address if the activity is not public", %{ + user: user, + other_user: other_user, + third_user: third_user + } do + expected_to = Enum.sort([user.ap_id]) + expected_cc = [third_user.ap_id] + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "@#{other_user.nickname} @#{third_user.nickname} bought a new swimsuit!", + "visibility" => "private" + }) + + %{"to" => to, "cc" => cc} = Utils.make_like_data(other_user, activity, nil) + assert Enum.sort(to) == expected_to + assert Enum.sort(cc) == expected_cc + end + end + + describe "fetch_ordered_collection" do + import Tesla.Mock + + test "fetches the first OrderedCollectionPage when an OrderedCollection is encountered" do + mock(fn + %{method: :get, url: "http://mastodon.com/outbox"} -> + json(%{"type" => "OrderedCollection", "first" => "http://mastodon.com/outbox?page=true"}) + + %{method: :get, url: "http://mastodon.com/outbox?page=true"} -> + json(%{"type" => "OrderedCollectionPage", "orderedItems" => ["ok"]}) + end) + + assert Utils.fetch_ordered_collection("http://mastodon.com/outbox", 1) == ["ok"] + end + + test "fetches several pages in the right order one after another, but only the specified amount" do + mock(fn + %{method: :get, url: "http://example.com/outbox"} -> + json(%{ + "type" => "OrderedCollectionPage", + "orderedItems" => [0], + "next" => "http://example.com/outbox?page=1" + }) + + %{method: :get, url: "http://example.com/outbox?page=1"} -> + json(%{ + "type" => "OrderedCollectionPage", + "orderedItems" => [1], + "next" => "http://example.com/outbox?page=2" + }) + + %{method: :get, url: "http://example.com/outbox?page=2"} -> + json(%{"type" => "OrderedCollectionPage", "orderedItems" => [2]}) + end) + + assert Utils.fetch_ordered_collection("http://example.com/outbox", 0) == [0] + assert Utils.fetch_ordered_collection("http://example.com/outbox", 1) == [0, 1] + end + + test "returns an error if the url doesn't have an OrderedCollection/Page" do + mock(fn + %{method: :get, url: "http://example.com/not-an-outbox"} -> + json(%{"type" => "NotAnOutbox"}) + end) + + assert {:error, _} = Utils.fetch_ordered_collection("http://example.com/not-an-outbox", 1) + end + + test "returns the what was collected if there are less pages than specified" do + mock(fn + %{method: :get, url: "http://example.com/outbox"} -> + json(%{ + "type" => "OrderedCollectionPage", + "orderedItems" => [0], + "next" => "http://example.com/outbox?page=1" + }) + + %{method: :get, url: "http://example.com/outbox?page=1"} -> + json(%{"type" => "OrderedCollectionPage", "orderedItems" => [1]}) + end) + + assert Utils.fetch_ordered_collection("http://example.com/outbox", 5) == [0, 1] + end + end + + test "make_json_ld_header/0" do + assert Utils.make_json_ld_header() == %{ + "@context" => [ + "https://www.w3.org/ns/activitystreams", + "http://localhost:4001/schemas/litepub-0.1.jsonld", + %{ + "@language" => "und" + } + ] + } + end +end diff --git a/test/web/activity_pub/views/object_view_test.exs b/test/web/activity_pub/views/object_view_test.exs index d144a77fc..d939fc5a7 100644 --- a/test/web/activity_pub/views/object_view_test.exs +++ b/test/web/activity_pub/views/object_view_test.exs @@ -2,8 +2,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectViewTest do use Pleroma.DataCase import Pleroma.Factory - alias Pleroma.Web.CommonAPI alias Pleroma.Web.ActivityPub.ObjectView + alias Pleroma.Web.CommonAPI test "renders a note object" do note = insert(:note) diff --git a/test/web/activity_pub/views/user_view_test.exs b/test/web/activity_pub/views/user_view_test.exs index 7fc870e96..9fb9455d2 100644 --- a/test/web/activity_pub/views/user_view_test.exs +++ b/test/web/activity_pub/views/user_view_test.exs @@ -15,4 +15,66 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do assert String.contains?(result["publicKey"]["publicKeyPem"], "BEGIN PUBLIC KEY") end + + test "Does not add an avatar image if the user hasn't set one" do + user = insert(:user) + {:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user) + + result = UserView.render("user.json", %{user: user}) + refute result["icon"] + refute result["image"] + + user = + insert(:user, + avatar: %{"url" => [%{"href" => "https://someurl"}]}, + info: %{ + banner: %{"url" => [%{"href" => "https://somebanner"}]} + } + ) + + {:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user) + + result = UserView.render("user.json", %{user: user}) + assert result["icon"]["url"] == "https://someurl" + assert result["image"]["url"] == "https://somebanner" + end + + describe "endpoints" do + test "local users have a usable endpoints structure" do + user = insert(:user) + {:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user) + + result = UserView.render("user.json", %{user: user}) + + assert result["id"] == user.ap_id + + %{ + "sharedInbox" => _, + "oauthAuthorizationEndpoint" => _, + "oauthRegistrationEndpoint" => _, + "oauthTokenEndpoint" => _ + } = result["endpoints"] + end + + test "remote users have an empty endpoints structure" do + user = insert(:user, local: false) + {:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user) + + result = UserView.render("user.json", %{user: user}) + + assert result["id"] == user.ap_id + assert result["endpoints"] == %{} + end + + test "instance users do not expose oAuth endpoints" do + user = insert(:user, nickname: nil, local: true) + {:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user) + + result = UserView.render("user.json", %{user: user}) + + refute result["endpoints"]["oauthAuthorizationEndpoint"] + refute result["endpoints"]["oauthRegistrationEndpoint"] + refute result["endpoints"]["oauthTokenEndpoint"] + end + end end diff --git a/test/web/activity_pub/visibilty_test.exs b/test/web/activity_pub/visibilty_test.exs new file mode 100644 index 000000000..24b96c4aa --- /dev/null +++ b/test/web/activity_pub/visibilty_test.exs @@ -0,0 +1,98 @@ +defmodule Pleroma.Web.ActivityPub.VisibilityTest do + use Pleroma.DataCase + + alias Pleroma.Web.ActivityPub.Visibility + alias Pleroma.Web.CommonAPI + import Pleroma.Factory + + setup do + user = insert(:user) + mentioned = insert(:user) + following = insert(:user) + unrelated = insert(:user) + {:ok, following} = Pleroma.User.follow(following, user) + + {:ok, public} = + CommonAPI.post(user, %{"status" => "@#{mentioned.nickname}", "visibility" => "public"}) + + {:ok, private} = + CommonAPI.post(user, %{"status" => "@#{mentioned.nickname}", "visibility" => "private"}) + + {:ok, direct} = + CommonAPI.post(user, %{"status" => "@#{mentioned.nickname}", "visibility" => "direct"}) + + {:ok, unlisted} = + CommonAPI.post(user, %{"status" => "@#{mentioned.nickname}", "visibility" => "unlisted"}) + + %{ + public: public, + private: private, + direct: direct, + unlisted: unlisted, + user: user, + mentioned: mentioned, + following: following, + unrelated: unrelated + } + end + + test "is_direct?", %{public: public, private: private, direct: direct, unlisted: unlisted} do + assert Visibility.is_direct?(direct) + refute Visibility.is_direct?(public) + refute Visibility.is_direct?(private) + refute Visibility.is_direct?(unlisted) + end + + test "is_public?", %{public: public, private: private, direct: direct, unlisted: unlisted} do + refute Visibility.is_public?(direct) + assert Visibility.is_public?(public) + refute Visibility.is_public?(private) + assert Visibility.is_public?(unlisted) + end + + test "is_private?", %{public: public, private: private, direct: direct, unlisted: unlisted} do + refute Visibility.is_private?(direct) + refute Visibility.is_private?(public) + assert Visibility.is_private?(private) + refute Visibility.is_private?(unlisted) + end + + test "visible_for_user?", %{ + public: public, + private: private, + direct: direct, + unlisted: unlisted, + user: user, + mentioned: mentioned, + following: following, + unrelated: unrelated + } do + # All visible to author + + assert Visibility.visible_for_user?(public, user) + assert Visibility.visible_for_user?(private, user) + assert Visibility.visible_for_user?(unlisted, user) + assert Visibility.visible_for_user?(direct, user) + + # All visible to a mentioned user + + assert Visibility.visible_for_user?(public, mentioned) + assert Visibility.visible_for_user?(private, mentioned) + assert Visibility.visible_for_user?(unlisted, mentioned) + assert Visibility.visible_for_user?(direct, mentioned) + + # DM not visible for just follower + + assert Visibility.visible_for_user?(public, following) + assert Visibility.visible_for_user?(private, following) + assert Visibility.visible_for_user?(unlisted, following) + refute Visibility.visible_for_user?(direct, following) + + # Public and unlisted visible for unrelated user + + assert Visibility.visible_for_user?(public, unrelated) + assert Visibility.visible_for_user?(unlisted, unrelated) + refute Visibility.visible_for_user?(private, unrelated) + refute Visibility.visible_for_user?(direct, unrelated) + end +end diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 9634ad7c5..b3167a861 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -1,10 +1,13 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do use Pleroma.Web.ConnCase - alias Pleroma.{Repo, User} - + alias Pleroma.User + alias Pleroma.UserInviteToken import Pleroma.Factory - import ExUnit.CaptureLog describe "/api/pleroma/admin/user" do test "Delete" do @@ -37,6 +40,157 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do end end + describe "/api/pleroma/admin/users/:nickname" do + test "Show", %{conn: conn} do + admin = insert(:user, info: %{is_admin: true}) + user = insert(:user) + + conn = + conn + |> assign(:user, admin) + |> get("/api/pleroma/admin/users/#{user.nickname}") + + expected = %{ + "deactivated" => false, + "id" => to_string(user.id), + "local" => true, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "tags" => [] + } + + assert expected == json_response(conn, 200) + end + + test "when the user doesn't exist", %{conn: conn} do + admin = insert(:user, info: %{is_admin: true}) + user = build(:user) + + conn = + conn + |> assign(:user, admin) + |> get("/api/pleroma/admin/users/#{user.nickname}") + + assert "Not found" == json_response(conn, 404) + end + end + + describe "/api/pleroma/admin/user/follow" do + test "allows to force-follow another user" do + admin = insert(:user, info: %{is_admin: true}) + user = insert(:user) + follower = insert(:user) + + build_conn() + |> assign(:user, admin) + |> put_req_header("accept", "application/json") + |> post("/api/pleroma/admin/user/follow", %{ + "follower" => follower.nickname, + "followed" => user.nickname + }) + + user = User.get_by_id(user.id) + follower = User.get_by_id(follower.id) + + assert User.following?(follower, user) + end + end + + describe "/api/pleroma/admin/user/unfollow" do + test "allows to force-unfollow another user" do + admin = insert(:user, info: %{is_admin: true}) + user = insert(:user) + follower = insert(:user) + + User.follow(follower, user) + + build_conn() + |> assign(:user, admin) + |> put_req_header("accept", "application/json") + |> post("/api/pleroma/admin/user/unfollow", %{ + "follower" => follower.nickname, + "followed" => user.nickname + }) + + user = User.get_by_id(user.id) + follower = User.get_by_id(follower.id) + + refute User.following?(follower, user) + end + end + + describe "PUT /api/pleroma/admin/users/tag" do + setup do + admin = insert(:user, info: %{is_admin: true}) + user1 = insert(:user, %{tags: ["x"]}) + user2 = insert(:user, %{tags: ["y"]}) + user3 = insert(:user, %{tags: ["unchanged"]}) + + conn = + build_conn() + |> assign(:user, admin) + |> put_req_header("accept", "application/json") + |> put( + "/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=#{ + user2.nickname + }&tags[]=foo&tags[]=bar" + ) + + %{conn: conn, user1: user1, user2: user2, user3: user3} + end + + test "it appends specified tags to users with specified nicknames", %{ + conn: conn, + user1: user1, + user2: user2 + } do + assert json_response(conn, :no_content) + assert User.get_by_id(user1.id).tags == ["x", "foo", "bar"] + assert User.get_by_id(user2.id).tags == ["y", "foo", "bar"] + end + + test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do + assert json_response(conn, :no_content) + assert User.get_by_id(user3.id).tags == ["unchanged"] + end + end + + describe "DELETE /api/pleroma/admin/users/tag" do + setup do + admin = insert(:user, info: %{is_admin: true}) + user1 = insert(:user, %{tags: ["x"]}) + user2 = insert(:user, %{tags: ["y", "z"]}) + user3 = insert(:user, %{tags: ["unchanged"]}) + + conn = + build_conn() + |> assign(:user, admin) + |> put_req_header("accept", "application/json") + |> delete( + "/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=#{ + user2.nickname + }&tags[]=x&tags[]=z" + ) + + %{conn: conn, user1: user1, user2: user2, user3: user3} + end + + test "it removes specified tags from users with specified nicknames", %{ + conn: conn, + user1: user1, + user2: user2 + } do + assert json_response(conn, :no_content) + assert User.get_by_id(user1.id).tags == [] + assert User.get_by_id(user2.id).tags == ["y"] + end + + test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do + assert json_response(conn, :no_content) + assert User.get_by_id(user3.id).tags == ["unchanged"] + end + end + describe "/api/pleroma/admin/permission_group" do test "GET is giving user_info" do admin = insert(:user, info: %{is_admin: true}) @@ -84,6 +238,161 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do end end + describe "PUT /api/pleroma/admin/activation_status" do + setup %{conn: conn} do + admin = insert(:user, info: %{is_admin: true}) + + conn = + conn + |> assign(:user, admin) + |> put_req_header("accept", "application/json") + + %{conn: conn} + end + + test "deactivates the user", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> put("/api/pleroma/admin/activation_status/#{user.nickname}", %{status: false}) + + user = User.get_by_id(user.id) + assert user.info.deactivated == true + assert json_response(conn, :no_content) + end + + test "activates the user", %{conn: conn} do + user = insert(:user, info: %{deactivated: true}) + + conn = + conn + |> put("/api/pleroma/admin/activation_status/#{user.nickname}", %{status: true}) + + user = User.get_by_id(user.id) + assert user.info.deactivated == false + assert json_response(conn, :no_content) + end + + test "returns 403 when requested by a non-admin", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> put("/api/pleroma/admin/activation_status/#{user.nickname}", %{status: false}) + + assert json_response(conn, :forbidden) + end + end + + describe "POST /api/pleroma/admin/email_invite, with valid config" do + setup do + registrations_open = Pleroma.Config.get([:instance, :registrations_open]) + invites_enabled = Pleroma.Config.get([:instance, :invites_enabled]) + Pleroma.Config.put([:instance, :registrations_open], false) + Pleroma.Config.put([:instance, :invites_enabled], true) + + on_exit(fn -> + Pleroma.Config.put([:instance, :registrations_open], registrations_open) + Pleroma.Config.put([:instance, :invites_enabled], invites_enabled) + :ok + end) + + [user: insert(:user, info: %{is_admin: true})] + end + + test "sends invitation and returns 204", %{conn: conn, user: user} do + recipient_email = "foo@bar.com" + recipient_name = "J. D." + + conn = + conn + |> assign(:user, user) + |> post("/api/pleroma/admin/email_invite?email=#{recipient_email}&name=#{recipient_name}") + + assert json_response(conn, :no_content) + + token_record = List.last(Pleroma.Repo.all(Pleroma.UserInviteToken)) + assert token_record + refute token_record.used + + notify_email = Pleroma.Config.get([:instance, :notify_email]) + instance_name = Pleroma.Config.get([:instance, :name]) + + email = + Pleroma.Emails.UserEmail.user_invitation_email( + user, + token_record, + recipient_email, + recipient_name + ) + + Swoosh.TestAssertions.assert_email_sent( + from: {instance_name, notify_email}, + to: {recipient_name, recipient_email}, + html_body: email.html_body + ) + end + + test "it returns 403 if requested by a non-admin", %{conn: conn} do + non_admin_user = insert(:user) + + conn = + conn + |> assign(:user, non_admin_user) + |> post("/api/pleroma/admin/email_invite?email=foo@bar.com&name=JD") + + assert json_response(conn, :forbidden) + end + end + + describe "POST /api/pleroma/admin/email_invite, with invalid config" do + setup do + [user: insert(:user, info: %{is_admin: true})] + end + + test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn, user: user} do + registrations_open = Pleroma.Config.get([:instance, :registrations_open]) + invites_enabled = Pleroma.Config.get([:instance, :invites_enabled]) + Pleroma.Config.put([:instance, :registrations_open], false) + Pleroma.Config.put([:instance, :invites_enabled], false) + + on_exit(fn -> + Pleroma.Config.put([:instance, :registrations_open], registrations_open) + Pleroma.Config.put([:instance, :invites_enabled], invites_enabled) + :ok + end) + + conn = + conn + |> assign(:user, user) + |> post("/api/pleroma/admin/email_invite?email=foo@bar.com&name=JD") + + assert json_response(conn, :internal_server_error) + end + + test "it returns 500 if `registrations_open` is enabled", %{conn: conn, user: user} do + registrations_open = Pleroma.Config.get([:instance, :registrations_open]) + invites_enabled = Pleroma.Config.get([:instance, :invites_enabled]) + Pleroma.Config.put([:instance, :registrations_open], true) + Pleroma.Config.put([:instance, :invites_enabled], true) + + on_exit(fn -> + Pleroma.Config.put([:instance, :registrations_open], registrations_open) + Pleroma.Config.put([:instance, :invites_enabled], invites_enabled) + :ok + end) + + conn = + conn + |> assign(:user, user) + |> post("/api/pleroma/admin/email_invite?email=foo@bar.com&name=JD") + + assert json_response(conn, :internal_server_error) + end + end + test "/api/pleroma/admin/invite_token" do admin = insert(:user, info: %{is_admin: true}) @@ -108,4 +417,368 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do assert conn.status == 200 end + + describe "GET /api/pleroma/admin/users" do + test "renders users array for the first page" do + admin = insert(:user, info: %{is_admin: true}) + user = insert(:user, local: false, tags: ["foo", "bar"]) + + conn = + build_conn() + |> assign(:user, admin) + |> get("/api/pleroma/admin/users?page=1") + + assert json_response(conn, 200) == %{ + "count" => 2, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => admin.info.deactivated, + "id" => admin.id, + "nickname" => admin.nickname, + "roles" => %{"admin" => true, "moderator" => false}, + "local" => true, + "tags" => [] + }, + %{ + "deactivated" => user.info.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => false, + "tags" => ["foo", "bar"] + } + ] + } + end + + test "renders empty array for the second page" do + admin = insert(:user, info: %{is_admin: true}) + insert(:user) + + conn = + build_conn() + |> assign(:user, admin) + |> get("/api/pleroma/admin/users?page=2") + + assert json_response(conn, 200) == %{ + "count" => 2, + "page_size" => 50, + "users" => [] + } + end + + test "regular search" do + admin = insert(:user, info: %{is_admin: true}) + user = insert(:user, nickname: "bob") + + conn = + build_conn() + |> assign(:user, admin) + |> get("/api/pleroma/admin/users?query=bo") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => user.info.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [] + } + ] + } + end + + test "regular search with page size" do + admin = insert(:user, info: %{is_admin: true}) + user = insert(:user, nickname: "aalice") + user2 = insert(:user, nickname: "alice") + + conn = + build_conn() + |> assign(:user, admin) + |> get("/api/pleroma/admin/users?query=a&page_size=1&page=1") + + assert json_response(conn, 200) == %{ + "count" => 2, + "page_size" => 1, + "users" => [ + %{ + "deactivated" => user.info.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [] + } + ] + } + + conn = + build_conn() + |> assign(:user, admin) + |> get("/api/pleroma/admin/users?query=a&page_size=1&page=2") + + assert json_response(conn, 200) == %{ + "count" => 2, + "page_size" => 1, + "users" => [ + %{ + "deactivated" => user2.info.deactivated, + "id" => user2.id, + "nickname" => user2.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [] + } + ] + } + end + + test "only local users" do + admin = insert(:user, info: %{is_admin: true}, nickname: "john") + user = insert(:user, nickname: "bob") + + insert(:user, nickname: "bobb", local: false) + + conn = + build_conn() + |> assign(:user, admin) + |> get("/api/pleroma/admin/users?query=bo&filters=local") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => user.info.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [] + } + ] + } + end + + test "only local users with no query" do + admin = insert(:user, info: %{is_admin: true}, nickname: "john") + user = insert(:user, nickname: "bob") + + insert(:user, nickname: "bobb", local: false) + + conn = + build_conn() + |> assign(:user, admin) + |> get("/api/pleroma/admin/users?filters=local") + + assert json_response(conn, 200) == %{ + "count" => 2, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => user.info.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [] + }, + %{ + "deactivated" => admin.info.deactivated, + "id" => admin.id, + "nickname" => admin.nickname, + "roles" => %{"admin" => true, "moderator" => false}, + "local" => true, + "tags" => [] + } + ] + } + end + + test "it works with multiple filters" do + admin = insert(:user, nickname: "john", info: %{is_admin: true}) + user = insert(:user, nickname: "bob", local: false, info: %{deactivated: true}) + + insert(:user, nickname: "ken", local: true, info: %{deactivated: true}) + insert(:user, nickname: "bobb", local: false, info: %{deactivated: false}) + + conn = + build_conn() + |> assign(:user, admin) + |> get("/api/pleroma/admin/users?filters=deactivated,external") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => user.info.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => user.local, + "tags" => [] + } + ] + } + end + end + + test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation" do + admin = insert(:user, info: %{is_admin: true}) + user = insert(:user) + + conn = + build_conn() + |> assign(:user, admin) + |> patch("/api/pleroma/admin/users/#{user.nickname}/toggle_activation") + + assert json_response(conn, 200) == + %{ + "deactivated" => !user.info.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [] + } + end + + describe "GET /api/pleroma/admin/invite_token" do + test "without options" do + admin = insert(:user, info: %{is_admin: true}) + + conn = + build_conn() + |> assign(:user, admin) + |> get("/api/pleroma/admin/invite_token") + + token = json_response(conn, 200) + invite = UserInviteToken.find_by_token!(token) + refute invite.used + refute invite.expires_at + refute invite.max_use + assert invite.invite_type == "one_time" + end + + test "with expires_at" do + admin = insert(:user, info: %{is_admin: true}) + + conn = + build_conn() + |> assign(:user, admin) + |> get("/api/pleroma/admin/invite_token", %{ + "invite" => %{"expires_at" => Date.to_string(Date.utc_today())} + }) + + token = json_response(conn, 200) + invite = UserInviteToken.find_by_token!(token) + + refute invite.used + assert invite.expires_at == Date.utc_today() + refute invite.max_use + assert invite.invite_type == "date_limited" + end + + test "with max_use" do + admin = insert(:user, info: %{is_admin: true}) + + conn = + build_conn() + |> assign(:user, admin) + |> get("/api/pleroma/admin/invite_token", %{ + "invite" => %{"max_use" => 150} + }) + + token = json_response(conn, 200) + invite = UserInviteToken.find_by_token!(token) + refute invite.used + refute invite.expires_at + assert invite.max_use == 150 + assert invite.invite_type == "reusable" + end + + test "with max use and expires_at" do + admin = insert(:user, info: %{is_admin: true}) + + conn = + build_conn() + |> assign(:user, admin) + |> get("/api/pleroma/admin/invite_token", %{ + "invite" => %{"max_use" => 150, "expires_at" => Date.to_string(Date.utc_today())} + }) + + token = json_response(conn, 200) + invite = UserInviteToken.find_by_token!(token) + refute invite.used + assert invite.expires_at == Date.utc_today() + assert invite.max_use == 150 + assert invite.invite_type == "reusable_date_limited" + end + end + + describe "GET /api/pleroma/admin/invites" do + test "no invites" do + admin = insert(:user, info: %{is_admin: true}) + + conn = + build_conn() + |> assign(:user, admin) + |> get("/api/pleroma/admin/invites") + + assert json_response(conn, 200) == %{"invites" => []} + end + + test "with invite" do + admin = insert(:user, info: %{is_admin: true}) + {:ok, invite} = UserInviteToken.create_invite() + + conn = + build_conn() + |> assign(:user, admin) + |> get("/api/pleroma/admin/invites") + + assert json_response(conn, 200) == %{ + "invites" => [ + %{ + "expires_at" => nil, + "id" => invite.id, + "invite_type" => "one_time", + "max_use" => nil, + "token" => invite.token, + "used" => false, + "uses" => 0 + } + ] + } + end + end + + describe "POST /api/pleroma/admin/revoke_invite" do + test "with token" do + admin = insert(:user, info: %{is_admin: true}) + {:ok, invite} = UserInviteToken.create_invite() + + conn = + build_conn() + |> assign(:user, admin) + |> post("/api/pleroma/admin/revoke_invite", %{"token" => invite.token}) + + assert json_response(conn, 200) == %{ + "expires_at" => nil, + "id" => invite.id, + "invite_type" => "one_time", + "max_use" => nil, + "token" => invite.token, + "used" => true, + "uses" => 0 + } + end + end end diff --git a/test/web/admin_api/search_test.exs b/test/web/admin_api/search_test.exs new file mode 100644 index 000000000..3950996ed --- /dev/null +++ b/test/web/admin_api/search_test.exs @@ -0,0 +1,88 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.SearchTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Web.AdminAPI.Search + + import Pleroma.Factory + + describe "search for admin" do + test "it ignores case" do + insert(:user, nickname: "papercoach") + insert(:user, nickname: "CanadaPaperCoach") + + {:ok, _results, count} = + Search.user(%{ + query: "paper", + local: false, + page: 1, + page_size: 50 + }) + + assert count == 2 + end + + test "it returns local/external users" do + insert(:user, local: true) + insert(:user, local: false) + insert(:user, local: false) + + {:ok, _results, local_count} = + Search.user(%{ + query: "", + local: true + }) + + {:ok, _results, external_count} = + Search.user(%{ + query: "", + external: true + }) + + assert local_count == 1 + assert external_count == 2 + end + + test "it returns active/deactivated users" do + insert(:user, info: %{deactivated: true}) + insert(:user, info: %{deactivated: true}) + insert(:user, info: %{deactivated: false}) + + {:ok, _results, active_count} = + Search.user(%{ + query: "", + active: true + }) + + {:ok, _results, deactivated_count} = + Search.user(%{ + query: "", + deactivated: true + }) + + assert active_count == 1 + assert deactivated_count == 2 + end + + test "it returns specific user" do + insert(:user) + insert(:user) + insert(:user, nickname: "bob", local: true, info: %{deactivated: false}) + + {:ok, _results, total_count} = Search.user(%{query: ""}) + + {:ok, _results, count} = + Search.user(%{ + query: "Bo", + active: true, + local: true + }) + + assert total_count == 3 + assert count == 1 + end + end +end diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index 3dc5f6f84..b9ed088e4 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -1,10 +1,34 @@ -defmodule Pleroma.Web.CommonAPI.Test do +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.CommonAPITest do use Pleroma.DataCase + alias Pleroma.Activity + alias Pleroma.User + alias Pleroma.Object alias Pleroma.Web.CommonAPI - alias Pleroma.{User, Object} import Pleroma.Factory + test "with the safe_dm_mention option set, it does not mention people beyond the initial tags" do + har = insert(:user) + jafnhar = insert(:user) + tridi = insert(:user) + option = Pleroma.Config.get([:instance, :safe_dm_mentions]) + Pleroma.Config.put([:instance, :safe_dm_mentions], true) + + {:ok, activity} = + CommonAPI.post(har, %{ + "status" => "@#{jafnhar.nickname} hey, i never want to see @#{tridi.nickname} again", + "visibility" => "direct" + }) + + refute tridi.ap_id in activity.recipients + assert jafnhar.ap_id in activity.recipients + Pleroma.Config.put([:instance, :safe_dm_mentions], option) + end + test "it de-duplicates tags" do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{"status" => "#2hu #2HU"}) @@ -14,6 +38,13 @@ defmodule Pleroma.Web.CommonAPI.Test do assert object.data["tag"] == ["2hu"] end + test "it adds emoji in the object" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{"status" => ":moominmamma:"}) + + assert activity.data["object"]["emoji"]["moominmamma"] + end + test "it adds emoji when updating profiles" do user = insert(:user, %{name: ":karjalanpiirakka:"}) @@ -57,4 +88,183 @@ defmodule Pleroma.Web.CommonAPI.Test do assert object.data["content"] == "<p><b>2hu</b></p>alert('xss')" end end + + describe "reactions" do + test "repeating a status" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"}) + + {:ok, %Activity{}, _} = CommonAPI.repeat(activity.id, user) + end + + test "favoriting a status" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"}) + + {:ok, %Activity{}, _} = CommonAPI.favorite(activity.id, user) + end + + test "retweeting a status twice returns an error" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"}) + {:ok, %Activity{}, _object} = CommonAPI.repeat(activity.id, user) + {:error, _} = CommonAPI.repeat(activity.id, user) + end + + test "favoriting a status twice returns an error" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"}) + {:ok, %Activity{}, _object} = CommonAPI.favorite(activity.id, user) + {:error, _} = CommonAPI.favorite(activity.id, user) + end + end + + describe "pinned statuses" do + setup do + Pleroma.Config.put([:instance, :max_pinned_statuses], 1) + + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!"}) + + [user: user, activity: activity] + end + + test "pin status", %{user: user, activity: activity} do + assert {:ok, ^activity} = CommonAPI.pin(activity.id, user) + + id = activity.id + user = refresh_record(user) + + assert %User{info: %{pinned_activities: [^id]}} = user + end + + test "only self-authored can be pinned", %{activity: activity} do + user = insert(:user) + + assert {:error, "Could not pin"} = CommonAPI.pin(activity.id, user) + end + + test "max pinned statuses", %{user: user, activity: activity_one} do + {:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"}) + + assert {:ok, ^activity_one} = CommonAPI.pin(activity_one.id, user) + + user = refresh_record(user) + + assert {:error, "You have already pinned the maximum number of statuses"} = + CommonAPI.pin(activity_two.id, user) + end + + test "unpin status", %{user: user, activity: activity} do + {:ok, activity} = CommonAPI.pin(activity.id, user) + + user = refresh_record(user) + + assert {:ok, ^activity} = CommonAPI.unpin(activity.id, user) + + user = refresh_record(user) + + assert %User{info: %{pinned_activities: []}} = user + end + + test "should unpin when deleting a status", %{user: user, activity: activity} do + {:ok, activity} = CommonAPI.pin(activity.id, user) + + user = refresh_record(user) + + assert {:ok, _} = CommonAPI.delete(activity.id, user) + + user = refresh_record(user) + + assert %User{info: %{pinned_activities: []}} = user + end + end + + describe "mute tests" do + setup do + user = insert(:user) + + activity = insert(:note_activity) + + [user: user, activity: activity] + end + + test "add mute", %{user: user, activity: activity} do + {:ok, _} = CommonAPI.add_mute(user, activity) + assert CommonAPI.thread_muted?(user, activity) + end + + test "remove mute", %{user: user, activity: activity} do + CommonAPI.add_mute(user, activity) + {:ok, _} = CommonAPI.remove_mute(user, activity) + refute CommonAPI.thread_muted?(user, activity) + end + + test "check that mutes can't be duplicate", %{user: user, activity: activity} do + CommonAPI.add_mute(user, activity) + {:error, _} = CommonAPI.add_mute(user, activity) + end + end + + describe "reports" do + test "creates a report" do + reporter = insert(:user) + target_user = insert(:user) + + {:ok, activity} = CommonAPI.post(target_user, %{"status" => "foobar"}) + + reporter_ap_id = reporter.ap_id + target_ap_id = target_user.ap_id + activity_ap_id = activity.data["id"] + comment = "foobar" + + report_data = %{ + "account_id" => target_user.id, + "comment" => comment, + "status_ids" => [activity.id] + } + + assert {:ok, flag_activity} = CommonAPI.report(reporter, report_data) + + assert %Activity{ + actor: ^reporter_ap_id, + data: %{ + "type" => "Flag", + "content" => ^comment, + "object" => [^target_ap_id, ^activity_ap_id] + } + } = flag_activity + end + end + + describe "reblog muting" do + setup do + muter = insert(:user) + + muted = insert(:user) + + [muter: muter, muted: muted] + end + + test "add a reblog mute", %{muter: muter, muted: muted} do + {:ok, muter} = CommonAPI.hide_reblogs(muter, muted) + + assert Pleroma.User.showing_reblogs?(muter, muted) == false + end + + test "remove a reblog mute", %{muter: muter, muted: muted} do + {:ok, muter} = CommonAPI.hide_reblogs(muter, muted) + {:ok, muter} = CommonAPI.show_reblogs(muter, muted) + + assert Pleroma.User.showing_reblogs?(muter, muted) == true + end + end end diff --git a/test/web/common_api/common_api_utils_test.exs b/test/web/common_api/common_api_utils_test.exs index b01ce04f8..f0c59d5c3 100644 --- a/test/web/common_api/common_api_utils_test.exs +++ b/test/web/common_api/common_api_utils_test.exs @@ -1,7 +1,12 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.CommonAPI.UtilsTest do + alias Pleroma.Builders.UserBuilder + alias Pleroma.Object alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.Endpoint - alias Pleroma.Builders.{UserBuilder} use Pleroma.DataCase test "it adds attachment links to a given text and attachment set" do @@ -52,4 +57,136 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do assert expected == Utils.emoji_from_profile(user) end + + describe "format_input/3" do + test "works for bare text/plain" do + text = "hello world!" + expected = "hello world!" + + {output, [], []} = Utils.format_input(text, "text/plain") + + assert output == expected + + text = "hello world!\n\nsecond paragraph!" + expected = "hello world!<br><br>second paragraph!" + + {output, [], []} = Utils.format_input(text, "text/plain") + + assert output == expected + end + + test "works for bare text/html" do + text = "<p>hello world!</p>" + expected = "<p>hello world!</p>" + + {output, [], []} = Utils.format_input(text, "text/html") + + assert output == expected + + text = "<p>hello world!</p>\n\n<p>second paragraph</p>" + expected = "<p>hello world!</p>\n\n<p>second paragraph</p>" + + {output, [], []} = Utils.format_input(text, "text/html") + + assert output == expected + end + + test "works for bare text/markdown" do + text = "**hello world**" + expected = "<p><strong>hello world</strong></p>\n" + + {output, [], []} = Utils.format_input(text, "text/markdown") + + assert output == expected + + text = "**hello world**\n\n*another paragraph*" + expected = "<p><strong>hello world</strong></p>\n<p><em>another paragraph</em></p>\n" + + {output, [], []} = Utils.format_input(text, "text/markdown") + + assert output == expected + + text = """ + > cool quote + + by someone + """ + + expected = "<blockquote><p>cool quote</p>\n</blockquote>\n<p>by someone</p>\n" + + {output, [], []} = Utils.format_input(text, "text/markdown") + + assert output == expected + end + + test "works for text/markdown with mentions" do + {:ok, user} = + UserBuilder.insert(%{nickname: "user__test", ap_id: "http://foo.com/user__test"}) + + text = "**hello world**\n\n*another @user__test and @user__test google.com paragraph*" + + expected = + "<p><strong>hello world</strong></p>\n<p><em>another <span class=\"h-card\"><a data-user=\"#{ + user.id + }\" class=\"u-url mention\" href=\"http://foo.com/user__test\">@<span>user__test</span></a></span> and <span class=\"h-card\"><a data-user=\"#{ + user.id + }\" class=\"u-url mention\" href=\"http://foo.com/user__test\">@<span>user__test</span></a></span> <a href=\"http://google.com\">google.com</a> paragraph</em></p>\n" + + {output, _, _} = Utils.format_input(text, "text/markdown") + + assert output == expected + end + end + + describe "context_to_conversation_id" do + test "creates a mapping object" do + conversation_id = Utils.context_to_conversation_id("random context") + object = Object.get_by_ap_id("random context") + + assert conversation_id == object.id + end + + test "returns an existing mapping for an existing object" do + {:ok, object} = Object.context_mapping("random context") |> Repo.insert() + conversation_id = Utils.context_to_conversation_id("random context") + + assert conversation_id == object.id + end + end + + describe "formats date to asctime" do + test "when date is in ISO 8601 format" do + date = DateTime.utc_now() |> DateTime.to_iso8601() + + expected = + date + |> DateTime.from_iso8601() + |> elem(1) + |> Calendar.Strftime.strftime!("%a %b %d %H:%M:%S %z %Y") + + assert Utils.date_to_asctime(date) == expected + end + + test "when date is a binary in wrong format" do + date = DateTime.utc_now() + + expected = "" + + assert Utils.date_to_asctime(date) == expected + end + + test "when date is a Unix timestamp" do + date = DateTime.utc_now() |> DateTime.to_unix() + + expected = "" + + assert Utils.date_to_asctime(date) == expected + end + + test "when date is nil" do + expected = "" + + assert Utils.date_to_asctime(nil) == expected + end + end end diff --git a/test/web/federator_test.exs b/test/web/federator_test.exs index 02e1ca76e..52729eb50 100644 --- a/test/web/federator_test.exs +++ b/test/web/federator_test.exs @@ -1,24 +1,18 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.FederatorTest do - alias Pleroma.Web.Federator + alias Pleroma.Instances alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Federator use Pleroma.DataCase import Pleroma.Factory import Mock - test "enqueues an element according to priority" do - queue = [%{item: 1, priority: 2}] - - new_queue = Federator.enqueue_sorted(queue, 2, 1) - assert new_queue == [%{item: 2, priority: 1}, %{item: 1, priority: 2}] - - new_queue = Federator.enqueue_sorted(queue, 2, 3) - assert new_queue == [%{item: 1, priority: 2}, %{item: 2, priority: 3}] - end - - test "pop first item" do - queue = [%{item: 2, priority: 1}, %{item: 1, priority: 2}] - - assert {2, [%{item: 1, priority: 2}]} = Federator.queue_pop(queue) + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok end describe "Publish an activity" do @@ -40,7 +34,7 @@ defmodule Pleroma.Web.FederatorTest do relay_mock: relay_mock } do with_mocks([relay_mock]) do - Federator.handle(:publish, activity) + Federator.publish(activity) end assert_received :relay_publish @@ -53,7 +47,7 @@ defmodule Pleroma.Web.FederatorTest do Pleroma.Config.put([:instance, :allow_relay], false) with_mocks([relay_mock]) do - Federator.handle(:publish, activity) + Federator.publish(activity) end refute_received :relay_publish @@ -62,6 +56,122 @@ defmodule Pleroma.Web.FederatorTest do end end + describe "Targets reachability filtering in `publish`" do + test_with_mock "it federates only to reachable instances via AP", + Federator, + [:passthrough], + [] do + user = insert(:user) + + {inbox1, inbox2} = + {"https://domain.com/users/nick1/inbox", "https://domain2.com/users/nick2/inbox"} + + insert(:user, %{ + local: false, + nickname: "nick1@domain.com", + ap_id: "https://domain.com/users/nick1", + info: %{ap_enabled: true, source_data: %{"inbox" => inbox1}} + }) + + insert(:user, %{ + local: false, + nickname: "nick2@domain2.com", + ap_id: "https://domain2.com/users/nick2", + info: %{ap_enabled: true, source_data: %{"inbox" => inbox2}} + }) + + dt = NaiveDateTime.utc_now() + Instances.set_unreachable(inbox1, dt) + + Instances.set_consistently_unreachable(URI.parse(inbox2).host) + + {:ok, _activity} = + CommonAPI.post(user, %{"status" => "HI @nick1@domain.com, @nick2@domain2.com!"}) + + assert called(Federator.publish_single_ap(%{inbox: inbox1, unreachable_since: dt})) + + refute called(Federator.publish_single_ap(%{inbox: inbox2})) + end + + test_with_mock "it federates only to reachable instances via Websub", + Federator, + [:passthrough], + [] do + user = insert(:user) + websub_topic = Pleroma.Web.OStatus.feed_path(user) + + sub1 = + insert(:websub_subscription, %{ + topic: websub_topic, + state: "active", + callback: "http://pleroma.soykaf.com/cb" + }) + + sub2 = + insert(:websub_subscription, %{ + topic: websub_topic, + state: "active", + callback: "https://pleroma2.soykaf.com/cb" + }) + + dt = NaiveDateTime.utc_now() + Instances.set_unreachable(sub2.callback, dt) + + Instances.set_consistently_unreachable(sub1.callback) + + {:ok, _activity} = CommonAPI.post(user, %{"status" => "HI"}) + + assert called( + Federator.publish_single_websub(%{ + callback: sub2.callback, + unreachable_since: dt + }) + ) + + refute called(Federator.publish_single_websub(%{callback: sub1.callback})) + end + + test_with_mock "it federates only to reachable instances via Salmon", + Federator, + [:passthrough], + [] do + user = insert(:user) + + remote_user1 = + insert(:user, %{ + local: false, + nickname: "nick1@domain.com", + ap_id: "https://domain.com/users/nick1", + info: %{salmon: "https://domain.com/salmon"} + }) + + remote_user2 = + insert(:user, %{ + local: false, + nickname: "nick2@domain2.com", + ap_id: "https://domain2.com/users/nick2", + info: %{salmon: "https://domain2.com/salmon"} + }) + + dt = NaiveDateTime.utc_now() + Instances.set_unreachable(remote_user2.ap_id, dt) + + Instances.set_consistently_unreachable("domain.com") + + {:ok, _activity} = + CommonAPI.post(user, %{"status" => "HI @nick1@domain.com, @nick2@domain2.com!"}) + + assert called( + Federator.publish_single_salmon(%{ + recipient: remote_user2, + unreachable_since: dt + }) + ) + + refute called(Federator.publish_single_websub(%{recipient: remote_user1})) + end + end + describe "Receive an activity" do test "successfully processes incoming AP docs with correct origin" do params = %{ @@ -78,7 +188,7 @@ defmodule Pleroma.Web.FederatorTest do "to" => ["https://www.w3.org/ns/activitystreams#Public"] } - {:ok, _activity} = Federator.handle(:incoming_ap_doc, params) + {:ok, _activity} = Federator.incoming_ap_doc(params) end test "rejects incoming AP docs with incorrect origin" do @@ -96,7 +206,7 @@ defmodule Pleroma.Web.FederatorTest do "to" => ["https://www.w3.org/ns/activitystreams#Public"] } - :error = Federator.handle(:incoming_ap_doc, params) + :error = Federator.incoming_ap_doc(params) end end end diff --git a/test/web/http_sigs/http_sig_test.exs b/test/web/http_sigs/http_sig_test.exs index b2bf8d61b..c4d2eaf78 100644 --- a/test/web/http_sigs/http_sig_test.exs +++ b/test/web/http_sigs/http_sig_test.exs @@ -1,12 +1,19 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + # http signatures # Test data from https://tools.ietf.org/html/draft-cavage-http-signatures-08#appendix-C defmodule Pleroma.Web.HTTPSignaturesTest do use Pleroma.DataCase alias Pleroma.Web.HTTPSignatures import Pleroma.Factory + import Tesla.Mock - @private_key hd(:public_key.pem_decode(File.read!("test/web/http_sigs/priv.key"))) - |> :public_key.pem_entry_decode() + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end @public_key hd(:public_key.pem_decode(File.read!("test/web/http_sigs/pub.key"))) |> :public_key.pem_entry_decode() @@ -20,8 +27,6 @@ defmodule Pleroma.Web.HTTPSignaturesTest do "content-length" => "18" } - @body "{\"hello\": \"world\"}" - @default_signature """ keyId="Test",algorithm="rsa-sha256",signature="jKyvPcxB4JbmYY4mByyBY7cZfNl4OW9HpFQlG7N4YcJPteKTu4MWCLyk+gIr0wDgqtLWf9NLpMAMimdfsH7FSWGfbMFSrsVTHNTk0rK3usrfFnti1dxsM4jl0kYJCKTGI/UWkqiaxwNiKqGcdlEDrTcUhhsFsOIo8VhddmZTZ8w=" """ diff --git a/test/web/instances/instance_test.exs b/test/web/instances/instance_test.exs new file mode 100644 index 000000000..d28730994 --- /dev/null +++ b/test/web/instances/instance_test.exs @@ -0,0 +1,107 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Instances.InstanceTest do + alias Pleroma.Instances.Instance + alias Pleroma.Repo + + use Pleroma.DataCase + + import Pleroma.Factory + + setup_all do + config_path = [:instance, :federation_reachability_timeout_days] + initial_setting = Pleroma.Config.get(config_path) + + Pleroma.Config.put(config_path, 1) + on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end) + + :ok + end + + describe "set_reachable/1" do + test "clears `unreachable_since` of existing matching Instance record having non-nil `unreachable_since`" do + instance = insert(:instance, unreachable_since: NaiveDateTime.utc_now()) + + assert {:ok, instance} = Instance.set_reachable(instance.host) + refute instance.unreachable_since + end + + test "keeps nil `unreachable_since` of existing matching Instance record having nil `unreachable_since`" do + instance = insert(:instance, unreachable_since: nil) + + assert {:ok, instance} = Instance.set_reachable(instance.host) + refute instance.unreachable_since + end + + test "does NOT create an Instance record in case of no existing matching record" do + host = "domain.org" + assert nil == Instance.set_reachable(host) + + assert [] = Repo.all(Ecto.Query.from(i in Instance)) + assert Instance.reachable?(host) + end + end + + describe "set_unreachable/1" do + test "creates new record having `unreachable_since` to current time if record does not exist" do + assert {:ok, instance} = Instance.set_unreachable("https://domain.com/path") + + instance = Repo.get(Instance, instance.id) + assert instance.unreachable_since + assert "domain.com" == instance.host + end + + test "sets `unreachable_since` of existing record having nil `unreachable_since`" do + instance = insert(:instance, unreachable_since: nil) + refute instance.unreachable_since + + assert {:ok, _} = Instance.set_unreachable(instance.host) + + instance = Repo.get(Instance, instance.id) + assert instance.unreachable_since + end + + test "does NOT modify `unreachable_since` value of existing record in case it's present" do + instance = + insert(:instance, unreachable_since: NaiveDateTime.add(NaiveDateTime.utc_now(), -10)) + + assert instance.unreachable_since + initial_value = instance.unreachable_since + + assert {:ok, _} = Instance.set_unreachable(instance.host) + + instance = Repo.get(Instance, instance.id) + assert initial_value == instance.unreachable_since + end + end + + describe "set_unreachable/2" do + test "sets `unreachable_since` value of existing record in case it's newer than supplied value" do + instance = + insert(:instance, unreachable_since: NaiveDateTime.add(NaiveDateTime.utc_now(), -10)) + + assert instance.unreachable_since + + past_value = NaiveDateTime.add(NaiveDateTime.utc_now(), -100) + assert {:ok, _} = Instance.set_unreachable(instance.host, past_value) + + instance = Repo.get(Instance, instance.id) + assert past_value == instance.unreachable_since + end + + test "does NOT modify `unreachable_since` value of existing record in case it's equal to or older than supplied value" do + instance = + insert(:instance, unreachable_since: NaiveDateTime.add(NaiveDateTime.utc_now(), -10)) + + assert instance.unreachable_since + initial_value = instance.unreachable_since + + assert {:ok, _} = Instance.set_unreachable(instance.host, NaiveDateTime.utc_now()) + + instance = Repo.get(Instance, instance.id) + assert initial_value == instance.unreachable_since + end + end +end diff --git a/test/web/instances/instances_test.exs b/test/web/instances/instances_test.exs new file mode 100644 index 000000000..f0d84edea --- /dev/null +++ b/test/web/instances/instances_test.exs @@ -0,0 +1,132 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.InstancesTest do + alias Pleroma.Instances + + use Pleroma.DataCase + + setup_all do + config_path = [:instance, :federation_reachability_timeout_days] + initial_setting = Pleroma.Config.get(config_path) + + Pleroma.Config.put(config_path, 1) + on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end) + + :ok + end + + describe "reachable?/1" do + test "returns `true` for host / url with unknown reachability status" do + assert Instances.reachable?("unknown.site") + assert Instances.reachable?("http://unknown.site") + end + + test "returns `false` for host / url marked unreachable for at least `reachability_datetime_threshold()`" do + host = "consistently-unreachable.name" + Instances.set_consistently_unreachable(host) + + refute Instances.reachable?(host) + refute Instances.reachable?("http://#{host}/path") + end + + test "returns `true` for host / url marked unreachable for less than `reachability_datetime_threshold()`" do + url = "http://eventually-unreachable.name/path" + + Instances.set_unreachable(url) + + assert Instances.reachable?(url) + assert Instances.reachable?(URI.parse(url).host) + end + + test "returns true on non-binary input" do + assert Instances.reachable?(nil) + assert Instances.reachable?(1) + end + end + + describe "filter_reachable/1" do + setup do + host = "consistently-unreachable.name" + url1 = "http://eventually-unreachable.com/path" + url2 = "http://domain.com/path" + + Instances.set_consistently_unreachable(host) + Instances.set_unreachable(url1) + + result = Instances.filter_reachable([host, url1, url2, nil]) + %{result: result, url1: url1, url2: url2} + end + + test "returns a map with keys containing 'not marked consistently unreachable' elements of supplied list", + %{result: result, url1: url1, url2: url2} do + assert is_map(result) + assert Enum.sort([url1, url2]) == result |> Map.keys() |> Enum.sort() + end + + test "returns a map with `unreachable_since` values for keys", + %{result: result, url1: url1, url2: url2} do + assert is_map(result) + assert %NaiveDateTime{} = result[url1] + assert is_nil(result[url2]) + end + + test "returns an empty map for empty list or list containing no hosts / url" do + assert %{} == Instances.filter_reachable([]) + assert %{} == Instances.filter_reachable([nil]) + end + end + + describe "set_reachable/1" do + test "sets unreachable url or host reachable" do + host = "domain.com" + Instances.set_consistently_unreachable(host) + refute Instances.reachable?(host) + + Instances.set_reachable(host) + assert Instances.reachable?(host) + end + + test "keeps reachable url or host reachable" do + url = "https://site.name?q=" + assert Instances.reachable?(url) + + Instances.set_reachable(url) + assert Instances.reachable?(url) + end + + test "returns error status on non-binary input" do + assert {:error, _} = Instances.set_reachable(nil) + assert {:error, _} = Instances.set_reachable(1) + end + end + + # Note: implementation-specific (e.g. Instance) details of set_unreachable/1 + # should be tested in implementation-specific tests + describe "set_unreachable/1" do + test "returns error status on non-binary input" do + assert {:error, _} = Instances.set_unreachable(nil) + assert {:error, _} = Instances.set_unreachable(1) + end + end + + describe "set_consistently_unreachable/1" do + test "sets reachable url or host unreachable" do + url = "http://domain.com?q=" + assert Instances.reachable?(url) + + Instances.set_consistently_unreachable(url) + refute Instances.reachable?(url) + end + + test "keeps unreachable url or host unreachable" do + host = "site.name" + Instances.set_consistently_unreachable(host) + refute Instances.reachable?(host) + + Instances.set_consistently_unreachable(host) + refute Instances.reachable?(host) + end + end +end diff --git a/test/web/mastodon_api/account_view_test.exs b/test/web/mastodon_api/account_view_test.exs index a2d3a2547..d7487bed9 100644 --- a/test/web/mastodon_api/account_view_test.exs +++ b/test/web/mastodon_api/account_view_test.exs @@ -1,8 +1,12 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.MastodonAPI.AccountViewTest do use Pleroma.DataCase import Pleroma.Factory - alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.User + alias Pleroma.Web.MastodonAPI.AccountView test "Represent a user account" do source_data = %{ @@ -54,12 +58,33 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do note: "", privacy: "public", sensitive: false + }, + pleroma: %{ + confirmation_pending: false, + tags: [], + is_admin: false, + is_moderator: false, + relationship: %{} } } assert expected == AccountView.render("account.json", %{user: user}) end + test "Represent the user account for the account owner" do + user = insert(:user) + + notification_settings = %{ + "remote" => true, + "local" => true, + "followers" => true, + "follows" => true + } + + assert %{pleroma: %{notification_settings: ^notification_settings}} = + AccountView.render("account.json", %{user: user, for: user}) + end + test "Represent a Service(bot) account" do user = insert(:user, %{ @@ -91,6 +116,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do note: "", privacy: "public", sensitive: false + }, + pleroma: %{ + confirmation_pending: false, + tags: [], + is_admin: false, + is_moderator: false, + relationship: %{} } } @@ -124,12 +156,74 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do blocking: true, muting: false, muting_notifications: false, + subscribing: false, requested: false, domain_blocking: false, - showing_reblogs: false, + showing_reblogs: true, endorsed: false } assert expected == AccountView.render("relationship.json", %{user: user, target: other_user}) end + + test "represent an embedded relationship" do + user = + insert(:user, %{ + info: %{note_count: 5, follower_count: 3, source_data: %{"type" => "Service"}}, + nickname: "shp@shitposter.club", + inserted_at: ~N[2017-08-15 15:47:06.597036] + }) + + other_user = insert(:user) + + {:ok, other_user} = User.follow(other_user, user) + {:ok, other_user} = User.block(other_user, user) + + expected = %{ + id: to_string(user.id), + username: "shp", + acct: user.nickname, + display_name: user.name, + locked: false, + created_at: "2017-08-15T15:47:06.000Z", + followers_count: 3, + following_count: 0, + statuses_count: 5, + note: user.bio, + url: user.ap_id, + avatar: "http://localhost:4001/images/avi.png", + avatar_static: "http://localhost:4001/images/avi.png", + header: "http://localhost:4001/images/banner.png", + header_static: "http://localhost:4001/images/banner.png", + emojis: [], + fields: [], + bot: true, + source: %{ + note: "", + privacy: "public", + sensitive: false + }, + pleroma: %{ + confirmation_pending: false, + tags: [], + is_admin: false, + is_moderator: false, + relationship: %{ + id: to_string(user.id), + following: false, + followed_by: false, + blocking: true, + subscribing: false, + muting: false, + muting_notifications: false, + requested: false, + domain_blocking: false, + showing_reblogs: true, + endorsed: false + } + } + } + + assert expected == AccountView.render("account.json", %{user: user, for: other_user}) + end end diff --git a/test/web/mastodon_api/list_view_test.exs b/test/web/mastodon_api/list_view_test.exs index 5e36872ed..73143467f 100644 --- a/test/web/mastodon_api/list_view_test.exs +++ b/test/web/mastodon_api/list_view_test.exs @@ -1,8 +1,11 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.MastodonAPI.ListViewTest do use Pleroma.DataCase import Pleroma.Factory alias Pleroma.Web.MastodonAPI.ListView - alias Pleroma.List test "Represent a list" do user = insert(:user) diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index c30f253d9..f21cf677d 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -1,13 +1,32 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do use Pleroma.Web.ConnCase - alias Pleroma.Web.TwitterAPI.TwitterAPI - alias Pleroma.{Repo, User, Activity, Notification, Object} - alias Pleroma.Web.{OStatus, CommonAPI} + alias Ecto.Changeset + alias Pleroma.Activity + alias Pleroma.Notification + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.ScheduledActivity + alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub - + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.MastodonAPI.FilterView + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OStatus + alias Pleroma.Web.Push + alias Pleroma.Web.TwitterAPI.TwitterAPI import Pleroma.Factory import ExUnit.CaptureLog + import Tesla.Mock + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end test "the home timeline", %{conn: conn} do user = insert(:user) @@ -20,7 +39,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do |> assign(:user, user) |> get("/api/v1/timelines/home") - assert length(json_response(conn, 200)) == 0 + assert Enum.empty?(json_response(conn, 200)) {:ok, user} = User.follow(user, following) @@ -83,7 +102,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert %{"content" => "cofe", "id" => id, "spoiler_text" => "2hu", "sensitive" => false} = json_response(conn_one, 200) - assert Repo.get(Activity, id) + assert Activity.get_by_id(id) conn_two = conn @@ -122,7 +141,72 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do |> post("/api/v1/statuses", %{"status" => "cofe", "sensitive" => true}) assert %{"content" => "cofe", "id" => id, "sensitive" => true} = json_response(conn, 200) - assert Repo.get(Activity, id) + assert Activity.get_by_id(id) + end + + test "posting a fake status", %{conn: conn} do + user = insert(:user) + + real_conn = + conn + |> assign(:user, user) + |> post("/api/v1/statuses", %{ + "status" => + "\"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" + }) + + real_status = json_response(real_conn, 200) + + assert real_status + assert Object.get_by_ap_id(real_status["uri"]) + + real_status = + real_status + |> Map.put("id", nil) + |> Map.put("url", nil) + |> Map.put("uri", nil) + |> Map.put("created_at", nil) + |> Kernel.put_in(["pleroma", "conversation_id"], nil) + + fake_conn = + conn + |> assign(:user, user) + |> post("/api/v1/statuses", %{ + "status" => + "\"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", + "preview" => true + }) + + fake_status = json_response(fake_conn, 200) + + assert fake_status + refute Object.get_by_ap_id(fake_status["uri"]) + + fake_status = + fake_status + |> Map.put("id", nil) + |> Map.put("url", nil) + |> Map.put("uri", nil) + |> Map.put("created_at", nil) + |> Kernel.put_in(["pleroma", "conversation_id"], nil) + + assert real_status == fake_status + end + + test "posting a status with OGP link preview", %{conn: conn} do + Pleroma.Config.put([:rich_media, :enabled], true) + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> post("/api/v1/statuses", %{ + "status" => "http://example.com/ogp" + }) + + assert %{"id" => id, "card" => %{"title" => "The Rock"}} = json_response(conn, 200) + assert Activity.get_by_id(id) + Pleroma.Config.put([:rich_media, :enabled], false) end test "posting a direct status", %{conn: conn} do @@ -136,8 +220,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do |> post("api/v1/statuses", %{"status" => content, "visibility" => "direct"}) assert %{"id" => id, "visibility" => "direct"} = json_response(conn, 200) - assert activity = Repo.get(Activity, id) - assert activity.recipients == [user2.ap_id] + assert activity = Activity.get_by_id(id) + assert activity.recipients == [user2.ap_id, user1.ap_id] assert activity.data["to"] == [user2.ap_id] assert activity.data["cc"] == [] end @@ -171,6 +255,16 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert %{"visibility" => "direct"} = status assert status["url"] != direct.data["id"] + # User should be able to see his own direct message + res_conn = + build_conn() + |> assign(:user, user_one) + |> get("api/v1/timelines/direct") + + [status] = json_response(res_conn, 200) + + assert %{"visibility" => "direct"} = status + # Both should be visible here res_conn = conn @@ -206,6 +300,33 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert status["url"] != direct.data["id"] end + test "doesn't include DMs from blocked users", %{conn: conn} do + blocker = insert(:user) + blocked = insert(:user) + user = insert(:user) + {:ok, blocker} = User.block(blocker, blocked) + + {:ok, _blocked_direct} = + CommonAPI.post(blocked, %{ + "status" => "Hi @#{blocker.nickname}!", + "visibility" => "direct" + }) + + {:ok, direct} = + CommonAPI.post(user, %{ + "status" => "Hi @#{blocker.nickname}!", + "visibility" => "direct" + }) + + res_conn = + conn + |> assign(:user, user) + |> get("api/v1/timelines/direct") + + [status] = json_response(res_conn, 200) + assert status["id"] == direct.id + end + test "replying to a status", %{conn: conn} do user = insert(:user) @@ -218,11 +339,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert %{"content" => "xD", "id" => id} = json_response(conn, 200) - activity = Repo.get(Activity, id) - object = Object.normalize(activity.data["object"]) + activity = Activity.get_by_id(id) assert activity.data["context"] == replied_to.data["context"] - assert object.data["inReplyToStatusId"] == replied_to.id + assert Activity.get_in_reply_to_activity(activity).id == replied_to.id end test "posting a status with an invalid in_reply_to_id", %{conn: conn} do @@ -235,7 +355,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert %{"content" => "xD", "id" => id} = json_response(conn, 200) - activity = Repo.get(Activity, id) + activity = Activity.get_by_id(id) assert activity end @@ -264,6 +384,53 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert id == to_string(user.id) end + test "apps/verify_credentials", %{conn: conn} do + token = insert(:oauth_token) + + conn = + conn + |> assign(:user, token.user) + |> assign(:token, token) + |> get("/api/v1/apps/verify_credentials") + + app = Repo.preload(token, :app).app + + expected = %{ + "name" => app.client_name, + "website" => app.website, + "vapid_key" => Push.vapid_config() |> Keyword.get(:public_key) + } + + assert expected == json_response(conn, 200) + end + + test "creates an oauth app", %{conn: conn} do + user = insert(:user) + app_attrs = build(:oauth_app) + + conn = + conn + |> assign(:user, user) + |> post("/api/v1/apps", %{ + client_name: app_attrs.client_name, + redirect_uris: app_attrs.redirect_uris + }) + + [app] = Repo.all(App) + + expected = %{ + "name" => app.client_name, + "website" => app.website, + "client_id" => app.client_id, + "client_secret" => app.client_secret, + "id" => app.id |> to_string(), + "redirect_uri" => app.redirect_uris, + "vapid_key" => Push.vapid_config() |> Keyword.get(:public_key) + } + + assert expected == json_response(conn, 200) + end + test "get a status", %{conn: conn} do activity = insert(:note_activity) @@ -287,7 +454,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert %{} = json_response(conn, 200) - assert Repo.get(Activity, activity.id) == nil + refute Activity.get_by_id(activity.id) end test "when you didn't create it", %{conn: conn} do @@ -301,7 +468,31 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert %{"error" => _} = json_response(conn, 403) - assert Repo.get(Activity, activity.id) == activity + assert Activity.get_by_id(activity.id) == activity + end + + test "when you're an admin or moderator", %{conn: conn} do + activity1 = insert(:note_activity) + activity2 = insert(:note_activity) + admin = insert(:user, info: %{is_admin: true}) + moderator = insert(:user, info: %{is_moderator: true}) + + res_conn = + conn + |> assign(:user, admin) + |> delete("/api/v1/statuses/#{activity1.id}") + + assert %{} = json_response(res_conn, 200) + + res_conn = + conn + |> assign(:user, moderator) + |> delete("/api/v1/statuses/#{activity2.id}") + + assert %{} = json_response(res_conn, 200) + + refute Activity.get_by_id(activity1.id) + refute Activity.get_by_id(activity2.id) end end @@ -346,12 +537,18 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do {:ok, filter_one} = Pleroma.Filter.create(query_one) {:ok, filter_two} = Pleroma.Filter.create(query_two) - conn = + response = conn |> assign(:user, user) |> get("/api/v1/filters") - - assert response = json_response(conn, 200) + |> json_response(200) + + assert response == + render_json( + FilterView, + "filters.json", + filters: [filter_two, filter_one] + ) end test "get a filter", %{conn: conn} do @@ -371,7 +568,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do |> assign(:user, user) |> get("/api/v1/filters/#{filter.filter_id}") - assert response = json_response(conn, 200) + assert _response = json_response(conn, 200) end test "update a filter", %{conn: conn} do @@ -384,7 +581,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do context: ["home"] } - {:ok, filter} = Pleroma.Filter.create(query) + {:ok, _filter} = Pleroma.Filter.create(query) new = %Pleroma.Filter{ phrase: "nii", @@ -549,7 +746,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do other_user = insert(:user) {:ok, activity_one} = TwitterAPI.create_status(other_user, %{"status" => "Marisa is cute."}) - {:ok, activity_two} = + {:ok, _activity_two} = TwitterAPI.create_status(other_user, %{ "status" => "Marisa is cute.", "visibility" => "private" @@ -585,7 +782,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do |> get("/api/v1/notifications") expected_response = - "hi <span><a href=\"#{user.ap_id}\">@<span>#{user.nickname}</span></a></span>" + "hi <span class=\"h-card\"><a data-user=\"#{user.id}\" class=\"u-url mention\" href=\"#{ + user.ap_id + }\">@<span>#{user.nickname}</span></a></span>" assert [%{"status" => %{"content" => response}} | _rest] = json_response(conn, 200) assert response == expected_response @@ -606,7 +805,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do |> get("/api/v1/notifications/#{notification.id}") expected_response = - "hi <span><a href=\"#{user.ap_id}\">@<span>#{user.nickname}</span></a></span>" + "hi <span class=\"h-card\"><a data-user=\"#{user.id}\" class=\"u-url mention\" href=\"#{ + user.ap_id + }\">@<span>#{user.nickname}</span></a></span>" assert %{"status" => %{"content" => response}} = json_response(conn, 200) assert response == expected_response @@ -653,6 +854,148 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert all = json_response(conn, 200) assert all == [] end + + test "paginates notifications using min_id, since_id, max_id, and limit", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity1} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"}) + {:ok, activity2} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"}) + {:ok, activity3} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"}) + {:ok, activity4} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"}) + + notification1_id = Repo.get_by(Notification, activity_id: activity1.id).id |> to_string() + notification2_id = Repo.get_by(Notification, activity_id: activity2.id).id |> to_string() + notification3_id = Repo.get_by(Notification, activity_id: activity3.id).id |> to_string() + notification4_id = Repo.get_by(Notification, activity_id: activity4.id).id |> to_string() + + conn = + conn + |> assign(:user, user) + + # min_id + conn_res = + conn + |> get("/api/v1/notifications?limit=2&min_id=#{notification1_id}") + + result = json_response(conn_res, 200) + assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result + + # since_id + conn_res = + conn + |> get("/api/v1/notifications?limit=2&since_id=#{notification1_id}") + + result = json_response(conn_res, 200) + assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result + + # max_id + conn_res = + conn + |> get("/api/v1/notifications?limit=2&max_id=#{notification4_id}") + + result = json_response(conn_res, 200) + assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result + end + + test "filters notifications using exclude_types", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, mention_activity} = CommonAPI.post(other_user, %{"status" => "hey @#{user.nickname}"}) + {:ok, create_activity} = CommonAPI.post(user, %{"status" => "hey"}) + {:ok, favorite_activity, _} = CommonAPI.favorite(create_activity.id, other_user) + {:ok, reblog_activity, _} = CommonAPI.repeat(create_activity.id, other_user) + {:ok, _, _, follow_activity} = CommonAPI.follow(other_user, user) + + mention_notification_id = + Repo.get_by(Notification, activity_id: mention_activity.id).id |> to_string() + + favorite_notification_id = + Repo.get_by(Notification, activity_id: favorite_activity.id).id |> to_string() + + reblog_notification_id = + Repo.get_by(Notification, activity_id: reblog_activity.id).id |> to_string() + + follow_notification_id = + Repo.get_by(Notification, activity_id: follow_activity.id).id |> to_string() + + conn = + conn + |> assign(:user, user) + + conn_res = + get(conn, "/api/v1/notifications", %{exclude_types: ["mention", "favourite", "reblog"]}) + + assert [%{"id" => ^follow_notification_id}] = json_response(conn_res, 200) + + conn_res = + get(conn, "/api/v1/notifications", %{exclude_types: ["favourite", "reblog", "follow"]}) + + assert [%{"id" => ^mention_notification_id}] = json_response(conn_res, 200) + + conn_res = + get(conn, "/api/v1/notifications", %{exclude_types: ["reblog", "follow", "mention"]}) + + assert [%{"id" => ^favorite_notification_id}] = json_response(conn_res, 200) + + conn_res = + get(conn, "/api/v1/notifications", %{exclude_types: ["follow", "mention", "favourite"]}) + + assert [%{"id" => ^reblog_notification_id}] = json_response(conn_res, 200) + end + + test "destroy multiple", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity1} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"}) + {:ok, activity2} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"}) + {:ok, activity3} = CommonAPI.post(user, %{"status" => "hi @#{other_user.nickname}"}) + {:ok, activity4} = CommonAPI.post(user, %{"status" => "hi @#{other_user.nickname}"}) + + notification1_id = Repo.get_by(Notification, activity_id: activity1.id).id |> to_string() + notification2_id = Repo.get_by(Notification, activity_id: activity2.id).id |> to_string() + notification3_id = Repo.get_by(Notification, activity_id: activity3.id).id |> to_string() + notification4_id = Repo.get_by(Notification, activity_id: activity4.id).id |> to_string() + + conn = + conn + |> assign(:user, user) + + conn_res = + conn + |> get("/api/v1/notifications") + + result = json_response(conn_res, 200) + assert [%{"id" => ^notification2_id}, %{"id" => ^notification1_id}] = result + + conn2 = + conn + |> assign(:user, other_user) + + conn_res = + conn2 + |> get("/api/v1/notifications") + + result = json_response(conn_res, 200) + assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result + + conn_destroy = + conn + |> delete("/api/v1/notifications/destroy_multiple", %{ + "ids" => [notification1_id, notification2_id] + }) + + assert json_response(conn_destroy, 200) == %{} + + conn_res = + conn2 + |> get("/api/v1/notifications") + + result = json_response(conn_res, 200) + assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result + end end describe "reblogging" do @@ -665,8 +1008,41 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do |> assign(:user, user) |> post("/api/v1/statuses/#{activity.id}/reblog") - assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} = - json_response(conn, 200) + assert %{ + "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}, + "reblogged" => true + } = json_response(conn, 200) + + assert to_string(activity.id) == id + end + + test "reblogged status for another user", %{conn: conn} do + activity = insert(:note_activity) + user1 = insert(:user) + user2 = insert(:user) + user3 = insert(:user) + {:ok, reblog_activity1, _object} = CommonAPI.repeat(activity.id, user1) + {:ok, _, _object} = CommonAPI.repeat(activity.id, user2) + + conn_res = + conn + |> assign(:user, user3) + |> get("/api/v1/statuses/#{reblog_activity1.id}") + + assert %{ + "reblog" => %{"id" => id, "reblogged" => false, "reblogs_count" => 2}, + "reblogged" => false + } = json_response(conn_res, 200) + + conn_res = + conn + |> assign(:user, user2) + |> get("/api/v1/statuses/#{reblog_activity1.id}") + + assert %{ + "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 2}, + "reblogged" => true + } = json_response(conn_res, 200) assert to_string(activity.id) == id end @@ -805,7 +1181,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do } media = - TwitterAPI.upload(file, "json") + TwitterAPI.upload(file, user, "json") |> Poison.decode!() {:ok, image_post} = @@ -825,6 +1201,26 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert [%{"id" => id}] = json_response(conn, 200) assert id == to_string(image_post.id) end + + test "gets a user's statuses without reblogs", %{conn: conn} do + user = insert(:user) + {:ok, post} = CommonAPI.post(user, %{"status" => "HI!!!"}) + {:ok, _, _} = CommonAPI.repeat(post.id, user) + + conn = + conn + |> get("/api/v1/accounts/#{user.id}/statuses", %{"exclude_reblogs" => "true"}) + + assert [%{"id" => id}] = json_response(conn, 200) + assert id == to_string(post.id) + + conn = + conn + |> get("/api/v1/accounts/#{user.id}/statuses", %{"exclude_reblogs" => "1"}) + + assert [%{"id" => id}] = json_response(conn, 200) + assert id == to_string(post.id) + end end describe "user relationships" do @@ -849,10 +1245,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do user = insert(:user, %{info: %Pleroma.User.Info{locked: true}}) other_user = insert(:user) - {:ok, activity} = ActivityPub.follow(other_user, user) + {:ok, _activity} = ActivityPub.follow(other_user, user) - user = Repo.get(User, user.id) - other_user = Repo.get(User, other_user.id) + user = User.get_by_id(user.id) + other_user = User.get_by_id(other_user.id) assert User.following?(other_user, user) == false @@ -866,13 +1262,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do end test "/api/v1/follow_requests/:id/authorize works" do - user = insert(:user, %{info: %Pleroma.User.Info{locked: true}}) + user = insert(:user, %{info: %User.Info{locked: true}}) other_user = insert(:user) - {:ok, activity} = ActivityPub.follow(other_user, user) + {:ok, _activity} = ActivityPub.follow(other_user, user) - user = Repo.get(User, user.id) - other_user = Repo.get(User, other_user.id) + user = User.get_by_id(user.id) + other_user = User.get_by_id(other_user.id) assert User.following?(other_user, user) == false @@ -884,8 +1280,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert relationship = json_response(conn, 200) assert to_string(other_user.id) == relationship["id"] - user = Repo.get(User, user.id) - other_user = Repo.get(User, other_user.id) + user = User.get_by_id(user.id) + other_user = User.get_by_id(other_user.id) assert User.following?(other_user, user) == true end @@ -906,7 +1302,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do user = insert(:user, %{info: %Pleroma.User.Info{locked: true}}) other_user = insert(:user) - {:ok, activity} = ActivityPub.follow(other_user, user) + {:ok, _activity} = ActivityPub.follow(other_user, user) + + user = User.get_by_id(user.id) conn = build_conn() @@ -916,8 +1314,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert relationship = json_response(conn, 200) assert to_string(other_user.id) == relationship["id"] - user = Repo.get(User, user.id) - other_user = Repo.get(User, other_user.id) + user = User.get_by_id(user.id) + other_user = User.get_by_id(other_user.id) assert User.following?(other_user, user) == false end @@ -940,6 +1338,17 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert %{"error" => "Can't find user"} = json_response(conn, 404) end + test "account fetching also works nickname", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> get("/api/v1/accounts/#{user.nickname}") + + assert %{"id" => id} = json_response(conn, 200) + assert id == user.id + end + test "media upload", %{conn: conn} do file = %Plug.Upload{ content_type: "image/jpg", @@ -960,6 +1369,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert media["type"] == "image" assert media["description"] == desc + assert media["id"] + + object = Repo.get(Object, media["id"]) + assert object.data["actor"] == User.ap_id(user) end test "hashtag timeline", %{conn: conn} do @@ -990,6 +1403,34 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do end) end + test "multi-hashtag timeline", %{conn: conn} do + user = insert(:user) + + {:ok, activity_test} = CommonAPI.post(user, %{"status" => "#test"}) + {:ok, activity_test1} = CommonAPI.post(user, %{"status" => "#test #test1"}) + {:ok, activity_none} = CommonAPI.post(user, %{"status" => "#test #none"}) + + any_test = + conn + |> get("/api/v1/timelines/tag/test", %{"any" => ["test1"]}) + + [status_none, status_test1, status_test] = json_response(any_test, 200) + + assert to_string(activity_test.id) == status_test["id"] + assert to_string(activity_test1.id) == status_test1["id"] + assert to_string(activity_none.id) == status_none["id"] + + restricted_test = + conn + |> get("/api/v1/timelines/tag/test", %{"all" => ["test1"], "none" => ["none"]}) + + assert [status_test1] == json_response(restricted_test, 200) + + all_test = conn |> get("/api/v1/timelines/tag/test", %{"all" => ["none"]}) + + assert [status_none] == json_response(all_test, 200) + end + test "getting followers", %{conn: conn} do user = insert(:user) other_user = insert(:user) @@ -1003,6 +1444,72 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert id == to_string(user.id) end + test "getting followers, hide_followers", %{conn: conn} do + user = insert(:user) + other_user = insert(:user, %{info: %{hide_followers: true}}) + {:ok, _user} = User.follow(user, other_user) + + conn = + conn + |> get("/api/v1/accounts/#{other_user.id}/followers") + + assert [] == json_response(conn, 200) + end + + test "getting followers, hide_followers, same user requesting", %{conn: conn} do + user = insert(:user) + other_user = insert(:user, %{info: %{hide_followers: true}}) + {:ok, _user} = User.follow(user, other_user) + + conn = + conn + |> assign(:user, other_user) + |> get("/api/v1/accounts/#{other_user.id}/followers") + + refute [] == json_response(conn, 200) + end + + test "getting followers, pagination", %{conn: conn} do + user = insert(:user) + follower1 = insert(:user) + follower2 = insert(:user) + follower3 = insert(:user) + {:ok, _} = User.follow(follower1, user) + {:ok, _} = User.follow(follower2, user) + {:ok, _} = User.follow(follower3, user) + + conn = + conn + |> assign(:user, user) + + res_conn = + conn + |> get("/api/v1/accounts/#{user.id}/followers?since_id=#{follower1.id}") + + assert [%{"id" => id3}, %{"id" => id2}] = json_response(res_conn, 200) + assert id3 == follower3.id + assert id2 == follower2.id + + res_conn = + conn + |> get("/api/v1/accounts/#{user.id}/followers?max_id=#{follower3.id}") + + assert [%{"id" => id2}, %{"id" => id1}] = json_response(res_conn, 200) + assert id2 == follower2.id + assert id1 == follower1.id + + res_conn = + conn + |> get("/api/v1/accounts/#{user.id}/followers?limit=1&max_id=#{follower3.id}") + + assert [%{"id" => id2}] = json_response(res_conn, 200) + assert id2 == follower2.id + + assert [link_header] = get_resp_header(res_conn, "link") + assert link_header =~ ~r/min_id=#{follower2.id}/ + assert link_header =~ ~r/max_id=#{follower2.id}/ + end + test "getting following", %{conn: conn} do user = insert(:user) other_user = insert(:user) @@ -1016,6 +1523,72 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert id == to_string(other_user.id) end + test "getting following, hide_follows", %{conn: conn} do + user = insert(:user, %{info: %{hide_follows: true}}) + other_user = insert(:user) + {:ok, user} = User.follow(user, other_user) + + conn = + conn + |> get("/api/v1/accounts/#{user.id}/following") + + assert [] == json_response(conn, 200) + end + + test "getting following, hide_follows, same user requesting", %{conn: conn} do + user = insert(:user, %{info: %{hide_follows: true}}) + other_user = insert(:user) + {:ok, user} = User.follow(user, other_user) + + conn = + conn + |> assign(:user, user) + |> get("/api/v1/accounts/#{user.id}/following") + + refute [] == json_response(conn, 200) + end + + test "getting following, pagination", %{conn: conn} do + user = insert(:user) + following1 = insert(:user) + following2 = insert(:user) + following3 = insert(:user) + {:ok, _} = User.follow(user, following1) + {:ok, _} = User.follow(user, following2) + {:ok, _} = User.follow(user, following3) + + conn = + conn + |> assign(:user, user) + + res_conn = + conn + |> get("/api/v1/accounts/#{user.id}/following?since_id=#{following1.id}") + + assert [%{"id" => id3}, %{"id" => id2}] = json_response(res_conn, 200) + assert id3 == following3.id + assert id2 == following2.id + + res_conn = + conn + |> get("/api/v1/accounts/#{user.id}/following?max_id=#{following3.id}") + + assert [%{"id" => id2}, %{"id" => id1}] = json_response(res_conn, 200) + assert id2 == following2.id + assert id1 == following1.id + + res_conn = + conn + |> get("/api/v1/accounts/#{user.id}/following?limit=1&max_id=#{following3.id}") + + assert [%{"id" => id2}] = json_response(res_conn, 200) + assert id2 == following2.id + + assert [link_header] = get_resp_header(res_conn, "link") + assert link_header =~ ~r/min_id=#{following2.id}/ + assert link_header =~ ~r/max_id=#{following2.id}/ + end + test "following / unfollowing a user", %{conn: conn} do user = insert(:user) other_user = insert(:user) @@ -1027,7 +1600,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert %{"id" => _id, "following" => true} = json_response(conn, 200) - user = Repo.get(User, user.id) + user = User.get_by_id(user.id) conn = build_conn() @@ -1036,7 +1609,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert %{"id" => _id, "following" => false} = json_response(conn, 200) - user = Repo.get(User, user.id) + user = User.get_by_id(user.id) conn = build_conn() @@ -1047,6 +1620,95 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert id == to_string(other_user.id) end + test "following / unfollowing errors" do + user = insert(:user) + + conn = + build_conn() + |> assign(:user, user) + + # self follow + conn_res = post(conn, "/api/v1/accounts/#{user.id}/follow") + assert %{"error" => "Record not found"} = json_response(conn_res, 404) + + # self unfollow + user = User.get_cached_by_id(user.id) + conn_res = post(conn, "/api/v1/accounts/#{user.id}/unfollow") + assert %{"error" => "Record not found"} = json_response(conn_res, 404) + + # self follow via uri + user = User.get_cached_by_id(user.id) + conn_res = post(conn, "/api/v1/follows", %{"uri" => user.nickname}) + assert %{"error" => "Record not found"} = json_response(conn_res, 404) + + # follow non existing user + conn_res = post(conn, "/api/v1/accounts/doesntexist/follow") + assert %{"error" => "Record not found"} = json_response(conn_res, 404) + + # follow non existing user via uri + conn_res = post(conn, "/api/v1/follows", %{"uri" => "doesntexist"}) + assert %{"error" => "Record not found"} = json_response(conn_res, 404) + + # unfollow non existing user + conn_res = post(conn, "/api/v1/accounts/doesntexist/unfollow") + assert %{"error" => "Record not found"} = json_response(conn_res, 404) + end + + test "muting / unmuting a user", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> post("/api/v1/accounts/#{other_user.id}/mute") + + assert %{"id" => _id, "muting" => true} = json_response(conn, 200) + + user = User.get_by_id(user.id) + + conn = + build_conn() + |> assign(:user, user) + |> post("/api/v1/accounts/#{other_user.id}/unmute") + + assert %{"id" => _id, "muting" => false} = json_response(conn, 200) + end + + test "subscribing / unsubscribing to a user", %{conn: conn} do + user = insert(:user) + subscription_target = insert(:user) + + conn = + conn + |> assign(:user, user) + |> post("/api/v1/pleroma/accounts/#{subscription_target.id}/subscribe") + + assert %{"id" => _id, "subscribing" => true} = json_response(conn, 200) + + conn = + build_conn() + |> assign(:user, user) + |> post("/api/v1/pleroma/accounts/#{subscription_target.id}/unsubscribe") + + assert %{"id" => _id, "subscribing" => false} = json_response(conn, 200) + end + + test "getting a list of mutes", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, user} = User.mute(user, other_user) + + conn = + conn + |> assign(:user, user) + |> get("/api/v1/mutes") + + other_user_id = to_string(other_user.id) + assert [%{"id" => ^other_user_id}] = json_response(conn, 200) + end + test "blocking / unblocking a user", %{conn: conn} do user = insert(:user) other_user = insert(:user) @@ -1058,7 +1720,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert %{"id" => _id, "blocking" => true} = json_response(conn, 200) - user = Repo.get(User, user.id) + user = User.get_by_id(user.id) conn = build_conn() @@ -1123,26 +1785,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert "even.worse.site" in domain_blocks end - test "unimplemented mute endpoints" do + test "unimplemented follow_requests, blocks, domain blocks" do user = insert(:user) - other_user = insert(:user) - ["mute", "unmute"] - |> Enum.each(fn endpoint -> - conn = - build_conn() - |> assign(:user, user) - |> post("/api/v1/accounts/#{other_user.id}/#{endpoint}") - - assert %{"id" => id} = json_response(conn, 200) - assert id == to_string(other_user.id) - end) - end - - test "unimplemented mutes, follow_requests, blocks, domain blocks" do - user = insert(:user) - - ["blocks", "domain_blocks", "mutes", "follow_requests"] + ["blocks", "domain_blocks", "follow_requests"] |> Enum.each(fn endpoint -> conn = build_conn() @@ -1223,6 +1869,24 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do end) end + test "search doesn't show statuses that it shouldn't", %{conn: conn} do + {:ok, activity} = + CommonAPI.post(insert(:user), %{ + "status" => "This is about 2hu, but private", + "visibility" => "private" + }) + + capture_log(fn -> + conn = + conn + |> get("/api/v1/search", %{"q" => activity.data["object"]["id"]}) + + assert results = json_response(conn, 200) + + [] = results["statuses"] + end) + end + test "search fetches remote accounts", %{conn: conn} do conn = conn @@ -1242,13 +1906,42 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do {:ok, _, _} = CommonAPI.favorite(activity.id, user) - conn = + first_conn = conn |> assign(:user, user) |> get("/api/v1/favourites") - assert [status] = json_response(conn, 200) + assert [status] = json_response(first_conn, 200) assert status["id"] == to_string(activity.id) + + assert [{"link", _link_header}] = + Enum.filter(first_conn.resp_headers, fn element -> match?({"link", _}, element) end) + + # Honours query params + {:ok, second_activity} = + CommonAPI.post(other_user, %{ + "status" => + "Trees Are Never Sad Look At Them Every Once In Awhile They're Quite Beautiful." + }) + + {:ok, _, _} = CommonAPI.favorite(second_activity.id, user) + + last_like = status["id"] + + second_conn = + conn + |> assign(:user, user) + |> get("/api/v1/favourites?since_id=#{last_like}") + + assert [second_status] = json_response(second_conn, 200) + assert second_status["id"] == to_string(second_activity.id) + + third_conn = + conn + |> assign(:user, user) + |> get("/api/v1/favourites?limit=0") + + assert [] = json_response(third_conn, 200) end describe "updating credentials" do @@ -1266,9 +1959,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert user = json_response(conn, 200) assert user["note"] == - "I drink <a href=\"http://localhost:4001/tag/cofe\">#cofe</a> with <span><a href=\"#{ - user2.ap_id - }\">@<span>#{user2.nickname}</span></a></span>" + ~s(I drink <a class="hashtag" data-tag="cofe" href="http://localhost:4001/tag/cofe" rel="tag">#cofe</a> with <span class="h-card"><a data-user=") <> + user2.id <> + ~s(" class="u-url mention" href=") <> + user2.ap_id <> ~s(">@<span>) <> user2.nickname <> ~s(</span></a></span>) end test "updates the user's locking status", %{conn: conn} do @@ -1330,24 +2024,800 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert user_response = json_response(conn, 200) assert user_response["header"] != User.banner_url(user) end + + test "requires 'write' permission", %{conn: conn} do + token1 = insert(:oauth_token, scopes: ["read"]) + token2 = insert(:oauth_token, scopes: ["write", "follow"]) + + for token <- [token1, token2] do + conn = + conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> patch("/api/v1/accounts/update_credentials", %{}) + + if token == token1 do + assert %{"error" => "Insufficient permissions: write."} == json_response(conn, 403) + else + assert json_response(conn, 200) + end + end + end end test "get instance information", %{conn: conn} do - insert(:user, %{local: true}) + conn = get(conn, "/api/v1/instance") + assert result = json_response(conn, 200) + + email = Pleroma.Config.get([:instance, :email]) + # Note: not checking for "max_toot_chars" since it's optional + assert %{ + "uri" => _, + "title" => _, + "description" => _, + "version" => _, + "email" => from_config_email, + "urls" => %{ + "streaming_api" => _ + }, + "stats" => _, + "thumbnail" => _, + "languages" => _, + "registrations" => _ + } = result + + assert email == from_config_email + end + + test "get instance stats", %{conn: conn} do user = insert(:user, %{local: true}) - insert(:user, %{local: false}) + + user2 = insert(:user, %{local: true}) + {:ok, _user2} = User.deactivate(user2, !user2.info.deactivated) + + insert(:user, %{local: false, nickname: "u@peer1.com"}) + insert(:user, %{local: false, nickname: "u@peer2.com"}) {:ok, _} = TwitterAPI.create_status(user, %{"status" => "cofe"}) + # Stats should count users with missing or nil `info.deactivated` value + user = User.get_by_id(user.id) + info_change = Changeset.change(user.info, %{deactivated: nil}) + + {:ok, _user} = + user + |> Changeset.change() + |> Changeset.put_embed(:info, info_change) + |> User.update_and_set_cache() + + Pleroma.Stats.update_stats() + + conn = get(conn, "/api/v1/instance") + + assert result = json_response(conn, 200) + + stats = result["stats"] + + assert stats + assert stats["user_count"] == 1 + assert stats["status_count"] == 1 + assert stats["domain_count"] == 2 + end + + test "get peers", %{conn: conn} do + insert(:user, %{local: false, nickname: "u@peer1.com"}) + insert(:user, %{local: false, nickname: "u@peer2.com"}) + Pleroma.Stats.update_stats() + conn = get(conn, "/api/v1/instance/peers") + + assert result = json_response(conn, 200) + + assert ["peer1.com", "peer2.com"] == Enum.sort(result) + end + + test "put settings", %{conn: conn} do + user = insert(:user) + conn = conn - |> get("/api/v1/instance") + |> assign(:user, user) + |> put("/api/web/settings", %{"data" => %{"programming" => "socks"}}) - assert result = json_response(conn, 200) + assert _result = json_response(conn, 200) + + user = User.get_cached_by_ap_id(user.ap_id) + assert user.info.settings == %{"programming" => "socks"} + end + + describe "pinned statuses" do + setup do + Pleroma.Config.put([:instance, :max_pinned_statuses], 1) + + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!"}) + + [user: user, activity: activity] + end + + test "returns pinned statuses", %{conn: conn, user: user, activity: activity} do + {:ok, _} = CommonAPI.pin(activity.id, user) + + result = + conn + |> assign(:user, user) + |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true") + |> json_response(200) + + id_str = to_string(activity.id) + + assert [%{"id" => ^id_str, "pinned" => true}] = result + end + + test "pin status", %{conn: conn, user: user, activity: activity} do + id_str = to_string(activity.id) + + assert %{"id" => ^id_str, "pinned" => true} = + conn + |> assign(:user, user) + |> post("/api/v1/statuses/#{activity.id}/pin") + |> json_response(200) + + assert [%{"id" => ^id_str, "pinned" => true}] = + conn + |> assign(:user, user) + |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true") + |> json_response(200) + end + + test "unpin status", %{conn: conn, user: user, activity: activity} do + {:ok, _} = CommonAPI.pin(activity.id, user) + + id_str = to_string(activity.id) + user = refresh_record(user) + + assert %{"id" => ^id_str, "pinned" => false} = + conn + |> assign(:user, user) + |> post("/api/v1/statuses/#{activity.id}/unpin") + |> json_response(200) + + assert [] = + conn + |> assign(:user, user) + |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true") + |> json_response(200) + end + + test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do + {:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"}) + + id_str_one = to_string(activity_one.id) + + assert %{"id" => ^id_str_one, "pinned" => true} = + conn + |> assign(:user, user) + |> post("/api/v1/statuses/#{id_str_one}/pin") + |> json_response(200) + + user = refresh_record(user) + + assert %{"error" => "You have already pinned the maximum number of statuses"} = + conn + |> assign(:user, user) + |> post("/api/v1/statuses/#{activity_two.id}/pin") + |> json_response(400) + end + + test "Status rich-media Card", %{conn: conn, user: user} do + Pleroma.Config.put([:rich_media, :enabled], true) + {:ok, activity} = CommonAPI.post(user, %{"status" => "http://example.com/ogp"}) + + response = + conn + |> get("/api/v1/statuses/#{activity.id}/card") + |> json_response(200) + + assert response == %{ + "image" => "http://ia.media-imdb.com/images/rock.jpg", + "provider_name" => "www.imdb.com", + "provider_url" => "http://www.imdb.com", + "title" => "The Rock", + "type" => "link", + "url" => "http://www.imdb.com/title/tt0117500/", + "description" => nil, + "pleroma" => %{ + "opengraph" => %{ + "image" => "http://ia.media-imdb.com/images/rock.jpg", + "title" => "The Rock", + "type" => "video.movie", + "url" => "http://www.imdb.com/title/tt0117500/" + } + } + } + + # works with private posts + {:ok, activity} = + CommonAPI.post(user, %{"status" => "http://example.com/ogp", "visibility" => "direct"}) + + response_two = + conn + |> assign(:user, user) + |> get("/api/v1/statuses/#{activity.id}/card") + |> json_response(200) + + assert response_two == response + + Pleroma.Config.put([:rich_media, :enabled], false) + end + end + + test "bookmarks" do + user = insert(:user) + for_user = insert(:user) + + {:ok, activity1} = + CommonAPI.post(user, %{ + "status" => "heweoo?" + }) + + {:ok, activity2} = + CommonAPI.post(user, %{ + "status" => "heweoo!" + }) + + response1 = + build_conn() + |> assign(:user, for_user) + |> post("/api/v1/statuses/#{activity1.id}/bookmark") + + assert json_response(response1, 200)["bookmarked"] == true + + response2 = + build_conn() + |> assign(:user, for_user) + |> post("/api/v1/statuses/#{activity2.id}/bookmark") + + assert json_response(response2, 200)["bookmarked"] == true + + bookmarks = + build_conn() + |> assign(:user, for_user) + |> get("/api/v1/bookmarks") + + assert [json_response(response2, 200), json_response(response1, 200)] == + json_response(bookmarks, 200) + + response1 = + build_conn() + |> assign(:user, for_user) + |> post("/api/v1/statuses/#{activity1.id}/unbookmark") + + assert json_response(response1, 200)["bookmarked"] == false + + bookmarks = + build_conn() + |> assign(:user, for_user) + |> get("/api/v1/bookmarks") + + assert [json_response(response2, 200)] == json_response(bookmarks, 200) + end + + describe "conversation muting" do + setup do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{"status" => "HIE"}) + + [user: user, activity: activity] + end + + test "mute conversation", %{conn: conn, user: user, activity: activity} do + id_str = to_string(activity.id) + + assert %{"id" => ^id_str, "muted" => true} = + conn + |> assign(:user, user) + |> post("/api/v1/statuses/#{activity.id}/mute") + |> json_response(200) + end + + test "unmute conversation", %{conn: conn, user: user, activity: activity} do + {:ok, _} = CommonAPI.add_mute(user, activity) + + id_str = to_string(activity.id) + user = refresh_record(user) + + assert %{"id" => ^id_str, "muted" => false} = + conn + |> assign(:user, user) + |> post("/api/v1/statuses/#{activity.id}/unmute") + |> json_response(200) + end + end + + test "flavours switching (Pleroma Extension)", %{conn: conn} do + user = insert(:user) + + get_old_flavour = + conn + |> assign(:user, user) + |> get("/api/v1/pleroma/flavour") + + assert "glitch" == json_response(get_old_flavour, 200) + + set_flavour = + conn + |> assign(:user, user) + |> post("/api/v1/pleroma/flavour/vanilla") + + assert "vanilla" == json_response(set_flavour, 200) + + get_new_flavour = + conn + |> assign(:user, user) + |> post("/api/v1/pleroma/flavour/vanilla") + + assert json_response(set_flavour, 200) == json_response(get_new_flavour, 200) + end + + describe "reports" do + setup do + reporter = insert(:user) + target_user = insert(:user) + + {:ok, activity} = CommonAPI.post(target_user, %{"status" => "foobar"}) + + [reporter: reporter, target_user: target_user, activity: activity] + end + + test "submit a basic report", %{conn: conn, reporter: reporter, target_user: target_user} do + assert %{"action_taken" => false, "id" => _} = + conn + |> assign(:user, reporter) + |> post("/api/v1/reports", %{"account_id" => target_user.id}) + |> json_response(200) + end + + test "submit a report with statuses and comment", %{ + conn: conn, + reporter: reporter, + target_user: target_user, + activity: activity + } do + assert %{"action_taken" => false, "id" => _} = + conn + |> assign(:user, reporter) + |> post("/api/v1/reports", %{ + "account_id" => target_user.id, + "status_ids" => [activity.id], + "comment" => "bad status!" + }) + |> json_response(200) + end + + test "account_id is required", %{ + conn: conn, + reporter: reporter, + activity: activity + } do + assert %{"error" => "Valid `account_id` required"} = + conn + |> assign(:user, reporter) + |> post("/api/v1/reports", %{"status_ids" => [activity.id]}) + |> json_response(400) + end + + test "comment must be up to the size specified in the config", %{ + conn: conn, + reporter: reporter, + target_user: target_user + } do + max_size = Pleroma.Config.get([:instance, :max_report_comment_size], 1000) + comment = String.pad_trailing("a", max_size + 1, "a") + + error = %{"error" => "Comment must be up to #{max_size} characters"} + + assert ^error = + conn + |> assign(:user, reporter) + |> post("/api/v1/reports", %{"account_id" => target_user.id, "comment" => comment}) + |> json_response(400) + end + end + + describe "link headers" do + test "preserves parameters in link headers", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity1} = + CommonAPI.post(other_user, %{ + "status" => "hi @#{user.nickname}", + "visibility" => "public" + }) + + {:ok, activity2} = + CommonAPI.post(other_user, %{ + "status" => "hi @#{user.nickname}", + "visibility" => "public" + }) + + notification1 = Repo.get_by(Notification, activity_id: activity1.id) + notification2 = Repo.get_by(Notification, activity_id: activity2.id) + + conn = + conn + |> assign(:user, user) + |> get("/api/v1/notifications", %{media_only: true}) + + assert [link_header] = get_resp_header(conn, "link") + assert link_header =~ ~r/media_only=true/ + assert link_header =~ ~r/min_id=#{notification2.id}/ + assert link_header =~ ~r/max_id=#{notification1.id}/ + end + end + + test "accounts fetches correct account for nicknames beginning with numbers", %{conn: conn} do + # Need to set an old-style integer ID to reproduce the problem + # (these are no longer assigned to new accounts but were preserved + # for existing accounts during the migration to flakeIDs) + user_one = insert(:user, %{id: 1212}) + user_two = insert(:user, %{nickname: "#{user_one.id}garbage"}) + + resp_one = + conn + |> get("/api/v1/accounts/#{user_one.id}") + + resp_two = + conn + |> get("/api/v1/accounts/#{user_two.nickname}") + + resp_three = + conn + |> get("/api/v1/accounts/#{user_two.id}") + + acc_one = json_response(resp_one, 200) + acc_two = json_response(resp_two, 200) + acc_three = json_response(resp_three, 200) + refute acc_one == acc_two + assert acc_two == acc_three + end + + describe "custom emoji" do + test "with tags", %{conn: conn} do + [emoji | _body] = + conn + |> get("/api/v1/custom_emojis") + |> json_response(200) + + assert Map.has_key?(emoji, "shortcode") + assert Map.has_key?(emoji, "static_url") + assert Map.has_key?(emoji, "tags") + assert is_list(emoji["tags"]) + assert Map.has_key?(emoji, "url") + assert Map.has_key?(emoji, "visible_in_picker") + end + end + + describe "index/2 redirections" do + setup %{conn: conn} do + session_opts = [ + store: :cookie, + key: "_test", + signing_salt: "cooldude" + ] + + conn = + conn + |> Plug.Session.call(Plug.Session.init(session_opts)) + |> fetch_session() + + test_path = "/web/statuses/test" + %{conn: conn, path: test_path} + end + + test "redirects not logged-in users to the login page", %{conn: conn, path: path} do + conn = get(conn, path) + + assert conn.status == 302 + assert redirected_to(conn) == "/web/login" + end + + test "does not redirect logged in users to the login page", %{conn: conn, path: path} do + token = insert(:oauth_token) + + conn = + conn + |> assign(:user, token.user) + |> put_session(:oauth_token, token.token) + |> get(path) + + assert conn.status == 200 + end + + test "saves referer path to session", %{conn: conn, path: path} do + conn = get(conn, path) + return_to = Plug.Conn.get_session(conn, :return_to) + + assert return_to == path + end + + test "redirects to the saved path after log in", %{conn: conn, path: path} do + app = insert(:oauth_app, client_name: "Mastodon-Local", redirect_uris: ".") + auth = insert(:oauth_authorization, app: app) + + conn = + conn + |> put_session(:return_to, path) + |> get("/web/login", %{code: auth.token}) + + assert conn.status == 302 + assert redirected_to(conn) == path + end + + test "redirects to the getting-started page when referer is not present", %{conn: conn} do + app = insert(:oauth_app, client_name: "Mastodon-Local", redirect_uris: ".") + auth = insert(:oauth_authorization, app: app) + + conn = get(conn, "/web/login", %{code: auth.token}) + + assert conn.status == 302 + assert redirected_to(conn) == "/web/getting-started" + end + end + + describe "scheduled activities" do + test "creates a scheduled activity", %{conn: conn} do + user = insert(:user) + scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond) + + conn = + conn + |> assign(:user, user) + |> post("/api/v1/statuses", %{ + "status" => "scheduled", + "scheduled_at" => scheduled_at + }) + + assert %{"scheduled_at" => expected_scheduled_at} = json_response(conn, 200) + assert expected_scheduled_at == Pleroma.Web.CommonAPI.Utils.to_masto_date(scheduled_at) + assert [] == Repo.all(Activity) + end + + test "creates a scheduled activity with a media attachment", %{conn: conn} do + user = insert(:user) + scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) + + conn = + conn + |> assign(:user, user) + |> post("/api/v1/statuses", %{ + "media_ids" => [to_string(upload.id)], + "status" => "scheduled", + "scheduled_at" => scheduled_at + }) + + assert %{"media_attachments" => [media_attachment]} = json_response(conn, 200) + assert %{"type" => "image"} = media_attachment + end + + test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now", + %{conn: conn} do + user = insert(:user) + + scheduled_at = + NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(5) - 1, :millisecond) + + conn = + conn + |> assign(:user, user) + |> post("/api/v1/statuses", %{ + "status" => "not scheduled", + "scheduled_at" => scheduled_at + }) + + assert %{"content" => "not scheduled"} = json_response(conn, 200) + assert [] == Repo.all(ScheduledActivity) + end + + test "returns error when daily user limit is exceeded", %{conn: conn} do + user = insert(:user) + + today = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(6), :millisecond) + |> NaiveDateTime.to_iso8601() + + attrs = %{params: %{}, scheduled_at: today} + {:ok, _} = ScheduledActivity.create(user, attrs) + {:ok, _} = ScheduledActivity.create(user, attrs) + + conn = + conn + |> assign(:user, user) + |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => today}) + + assert %{"error" => "daily limit exceeded"} == json_response(conn, 422) + end + + test "returns error when total user limit is exceeded", %{conn: conn} do + user = insert(:user) + + today = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(6), :millisecond) + |> NaiveDateTime.to_iso8601() + + tomorrow = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.hours(36), :millisecond) + |> NaiveDateTime.to_iso8601() + + attrs = %{params: %{}, scheduled_at: today} + {:ok, _} = ScheduledActivity.create(user, attrs) + {:ok, _} = ScheduledActivity.create(user, attrs) + {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow}) + + conn = + conn + |> assign(:user, user) + |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => tomorrow}) + + assert %{"error" => "total limit exceeded"} == json_response(conn, 422) + end + + test "shows scheduled activities", %{conn: conn} do + user = insert(:user) + scheduled_activity_id1 = insert(:scheduled_activity, user: user).id |> to_string() + scheduled_activity_id2 = insert(:scheduled_activity, user: user).id |> to_string() + scheduled_activity_id3 = insert(:scheduled_activity, user: user).id |> to_string() + scheduled_activity_id4 = insert(:scheduled_activity, user: user).id |> to_string() + + conn = + conn + |> assign(:user, user) + + # min_id + conn_res = + conn + |> get("/api/v1/scheduled_statuses?limit=2&min_id=#{scheduled_activity_id1}") + + result = json_response(conn_res, 200) + assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result + + # since_id + conn_res = + conn + |> get("/api/v1/scheduled_statuses?limit=2&since_id=#{scheduled_activity_id1}") + + result = json_response(conn_res, 200) + assert [%{"id" => ^scheduled_activity_id4}, %{"id" => ^scheduled_activity_id3}] = result + + # max_id + conn_res = + conn + |> get("/api/v1/scheduled_statuses?limit=2&max_id=#{scheduled_activity_id4}") + + result = json_response(conn_res, 200) + assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result + end + + test "shows a scheduled activity", %{conn: conn} do + user = insert(:user) + scheduled_activity = insert(:scheduled_activity, user: user) + + res_conn = + conn + |> assign(:user, user) + |> get("/api/v1/scheduled_statuses/#{scheduled_activity.id}") + + assert %{"id" => scheduled_activity_id} = json_response(res_conn, 200) + assert scheduled_activity_id == scheduled_activity.id |> to_string() + + res_conn = + conn + |> assign(:user, user) + |> get("/api/v1/scheduled_statuses/404") + + assert %{"error" => "Record not found"} = json_response(res_conn, 404) + end + + test "updates a scheduled activity", %{conn: conn} do + user = insert(:user) + scheduled_activity = insert(:scheduled_activity, user: user) + + new_scheduled_at = + NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond) + + res_conn = + conn + |> assign(:user, user) + |> put("/api/v1/scheduled_statuses/#{scheduled_activity.id}", %{ + scheduled_at: new_scheduled_at + }) + + assert %{"scheduled_at" => expected_scheduled_at} = json_response(res_conn, 200) + assert expected_scheduled_at == Pleroma.Web.CommonAPI.Utils.to_masto_date(new_scheduled_at) + + res_conn = + conn + |> assign(:user, user) + |> put("/api/v1/scheduled_statuses/404", %{scheduled_at: new_scheduled_at}) + + assert %{"error" => "Record not found"} = json_response(res_conn, 404) + end + + test "deletes a scheduled activity", %{conn: conn} do + user = insert(:user) + scheduled_activity = insert(:scheduled_activity, user: user) + + res_conn = + conn + |> assign(:user, user) + |> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}") + + assert %{} = json_response(res_conn, 200) + assert nil == Repo.get(ScheduledActivity, scheduled_activity.id) + + res_conn = + conn + |> assign(:user, user) + |> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}") + + assert %{"error" => "Record not found"} = json_response(res_conn, 404) + end + end + + test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{conn: conn} do + user1 = insert(:user) + user2 = insert(:user) + user3 = insert(:user) + + {:ok, replied_to} = TwitterAPI.create_status(user1, %{"status" => "cofe"}) + + # Reply to status from another user + conn1 = + conn + |> assign(:user, user2) + |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id}) + + assert %{"content" => "xD", "id" => id} = json_response(conn1, 200) + + activity = Activity.get_by_id(id) + + assert activity.data["object"]["inReplyTo"] == replied_to.data["object"]["id"] + assert Activity.get_in_reply_to_activity(activity).id == replied_to.id + + # Reblog from the third user + conn2 = + conn + |> assign(:user, user3) + |> post("/api/v1/statuses/#{activity.id}/reblog") + + assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} = + json_response(conn2, 200) + + assert to_string(activity.id) == id + + # Getting third user status + conn3 = + conn + |> assign(:user, user3) + |> get("api/v1/timelines/home") + + [reblogged_activity] = json_response(conn3, 200) + + assert reblogged_activity["reblog"]["in_reply_to_id"] == replied_to.id - assert result["stats"]["user_count"] == 2 - assert result["stats"]["status_count"] == 1 + replied_to_user = User.get_by_ap_id(replied_to.data["actor"]) + assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id end end diff --git a/test/web/mastodon_api/mastodon_socket_test.exs b/test/web/mastodon_api/mastodon_socket_test.exs deleted file mode 100644 index c7d71defc..000000000 --- a/test/web/mastodon_api/mastodon_socket_test.exs +++ /dev/null @@ -1,33 +0,0 @@ -defmodule Pleroma.Web.MastodonApi.MastodonSocketTest do - use Pleroma.DataCase - - alias Pleroma.Web.MastodonApi.MastodonSocket - alias Pleroma.Web.{Streamer, CommonAPI} - alias Pleroma.User - - import Pleroma.Factory - - test "public is working when non-authenticated" do - user = insert(:user) - - task = - Task.async(fn -> - assert_receive {:text, _}, 4_000 - end) - - fake_socket = %{ - transport_pid: task.pid, - assigns: %{} - } - - topics = %{ - "public" => [fake_socket] - } - - {:ok, activity} = CommonAPI.post(user, %{"status" => "Test"}) - - Streamer.push_to_socket(topics, "public", activity) - - Task.await(task) - end -end diff --git a/test/web/mastodon_api/notification_view_test.exs b/test/web/mastodon_api/notification_view_test.exs new file mode 100644 index 000000000..f2c1eb76c --- /dev/null +++ b/test/web/mastodon_api/notification_view_test.exs @@ -0,0 +1,104 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Notification + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.CommonAPI.Utils + alias Pleroma.Web.MastodonAPI.AccountView + alias Pleroma.Web.MastodonAPI.NotificationView + alias Pleroma.Web.MastodonAPI.StatusView + import Pleroma.Factory + + test "Mention notification" do + user = insert(:user) + mentioned_user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{mentioned_user.nickname}"}) + {:ok, [notification]} = Notification.create_notifications(activity) + user = User.get_by_id(user.id) + + expected = %{ + id: to_string(notification.id), + pleroma: %{is_seen: false}, + type: "mention", + account: AccountView.render("account.json", %{user: user, for: mentioned_user}), + status: StatusView.render("status.json", %{activity: activity, for: mentioned_user}), + created_at: Utils.to_masto_date(notification.inserted_at) + } + + result = + NotificationView.render("index.json", %{notifications: [notification], for: mentioned_user}) + + assert [expected] == result + end + + test "Favourite notification" do + user = insert(:user) + another_user = insert(:user) + {:ok, create_activity} = CommonAPI.post(user, %{"status" => "hey"}) + {:ok, favorite_activity, _object} = CommonAPI.favorite(create_activity.id, another_user) + {:ok, [notification]} = Notification.create_notifications(favorite_activity) + create_activity = Activity.get_by_id(create_activity.id) + + expected = %{ + id: to_string(notification.id), + pleroma: %{is_seen: false}, + type: "favourite", + account: AccountView.render("account.json", %{user: another_user, for: user}), + status: StatusView.render("status.json", %{activity: create_activity, for: user}), + created_at: Utils.to_masto_date(notification.inserted_at) + } + + result = NotificationView.render("index.json", %{notifications: [notification], for: user}) + + assert [expected] == result + end + + test "Reblog notification" do + user = insert(:user) + another_user = insert(:user) + {:ok, create_activity} = CommonAPI.post(user, %{"status" => "hey"}) + {:ok, reblog_activity, _object} = CommonAPI.repeat(create_activity.id, another_user) + {:ok, [notification]} = Notification.create_notifications(reblog_activity) + reblog_activity = Activity.get_by_id(create_activity.id) + + expected = %{ + id: to_string(notification.id), + pleroma: %{is_seen: false}, + type: "reblog", + account: AccountView.render("account.json", %{user: another_user, for: user}), + status: StatusView.render("status.json", %{activity: reblog_activity, for: user}), + created_at: Utils.to_masto_date(notification.inserted_at) + } + + result = NotificationView.render("index.json", %{notifications: [notification], for: user}) + + assert [expected] == result + end + + test "Follow notification" do + follower = insert(:user) + followed = insert(:user) + {:ok, follower, followed, _activity} = CommonAPI.follow(follower, followed) + notification = Notification |> Repo.one() |> Repo.preload(:activity) + + expected = %{ + id: to_string(notification.id), + pleroma: %{is_seen: false}, + type: "follow", + account: AccountView.render("account.json", %{user: follower, for: followed}), + created_at: Utils.to_masto_date(notification.inserted_at) + } + + result = + NotificationView.render("index.json", %{notifications: [notification], for: followed}) + + assert [expected] == result + end +end diff --git a/test/web/mastodon_api/push_subscription_view_test.exs b/test/web/mastodon_api/push_subscription_view_test.exs new file mode 100644 index 000000000..dc935fc82 --- /dev/null +++ b/test/web/mastodon_api/push_subscription_view_test.exs @@ -0,0 +1,23 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.PushSubscriptionViewTest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Web.MastodonAPI.PushSubscriptionView, as: View + alias Pleroma.Web.Push + + test "Represent a subscription" do + subscription = insert(:push_subscription, data: %{"alerts" => %{"mention" => true}}) + + expected = %{ + alerts: %{"mention" => true}, + endpoint: subscription.endpoint, + id: to_string(subscription.id), + server_key: Keyword.get(Push.vapid_config(), :public_key) + } + + assert expected == View.render("push_subscription.json", %{subscription: subscription}) + end +end diff --git a/test/web/mastodon_api/scheduled_activity_view_test.exs b/test/web/mastodon_api/scheduled_activity_view_test.exs new file mode 100644 index 000000000..ecbb855d4 --- /dev/null +++ b/test/web/mastodon_api/scheduled_activity_view_test.exs @@ -0,0 +1,68 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.ScheduledActivityViewTest do + use Pleroma.DataCase + alias Pleroma.ScheduledActivity + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.CommonAPI.Utils + alias Pleroma.Web.MastodonAPI.ScheduledActivityView + alias Pleroma.Web.MastodonAPI.StatusView + import Pleroma.Factory + + test "A scheduled activity with a media attachment" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{"status" => "hi"}) + + scheduled_at = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(:timer.minutes(10), :millisecond) + |> NaiveDateTime.to_iso8601() + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) + + attrs = %{ + params: %{ + "media_ids" => [upload.id], + "status" => "hi", + "sensitive" => true, + "spoiler_text" => "spoiler", + "visibility" => "unlisted", + "in_reply_to_id" => to_string(activity.id) + }, + scheduled_at: scheduled_at + } + + {:ok, scheduled_activity} = ScheduledActivity.create(user, attrs) + result = ScheduledActivityView.render("show.json", %{scheduled_activity: scheduled_activity}) + + expected = %{ + id: to_string(scheduled_activity.id), + media_attachments: + %{"media_ids" => [upload.id]} + |> Utils.attachments_from_ids() + |> Enum.map(&StatusView.render("attachment.json", %{attachment: &1})), + params: %{ + in_reply_to_id: to_string(activity.id), + media_ids: [upload.id], + poll: nil, + scheduled_at: nil, + sensitive: true, + spoiler_text: "spoiler", + text: "hi", + visibility: "unlisted" + }, + scheduled_at: Utils.to_masto_date(scheduled_activity.scheduled_at) + } + + assert expected == result + end +end diff --git a/test/web/mastodon_api/status_view_test.exs b/test/web/mastodon_api/status_view_test.exs index 4f58ce8af..4ea50c7c6 100644 --- a/test/web/mastodon_api/status_view_test.exs +++ b/test/web/mastodon_api/status_view_test.exs @@ -1,11 +1,57 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.MastodonAPI.StatusViewTest do use Pleroma.DataCase - alias Pleroma.Web.MastodonAPI.{StatusView, AccountView} - alias Pleroma.{Repo, User, Object} - alias Pleroma.Web.OStatus + alias Pleroma.Activity + alias Pleroma.User + alias Pleroma.Repo + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI + alias Pleroma.Web.CommonAPI.Utils + alias Pleroma.Web.MastodonAPI.AccountView + alias Pleroma.Web.MastodonAPI.StatusView + alias Pleroma.Web.OStatus import Pleroma.Factory + import Tesla.Mock + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + test "returns a temporary ap_id based user for activities missing db users" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"}) + + Repo.delete(user) + Cachex.clear(:user_cache) + + %{account: ms_user} = StatusView.render("status.json", activity: activity) + + assert ms_user.acct == "erroruser@example.com" + end + + test "tries to get a user by nickname if fetching by ap_id doesn't work" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"}) + + {:ok, user} = + user + |> Ecto.Changeset.change(%{ap_id: "#{user.ap_id}/extension/#{user.nickname}"}) + |> Repo.update() + + Cachex.clear(:user_cache) + + result = StatusView.render("status.json", activity: activity) + + assert result[:account][:id] == to_string(user.id) + end test "a note with null content" do note = insert(:note_activity) @@ -18,7 +64,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do Object.change(note_object, %{data: data}) |> Repo.update() - user = User.get_cached_by_ap_id(note.data["actor"]) + User.get_cached_by_ap_id(note.data["actor"]) status = StatusView.render("status.json", %{activity: note}) @@ -29,6 +75,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do note = insert(:note_activity) user = User.get_cached_by_ap_id(note.data["actor"]) + convo_id = Utils.context_to_conversation_id(note.data["object"]["context"]) + status = StatusView.render("status.json", %{activity: note}) created_at = @@ -38,10 +86,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do expected = %{ id: to_string(note.id), uri: note.data["object"]["id"], - url: note.data["object"]["id"], + url: Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, note), account: AccountView.render("account.json", %{user: user}), in_reply_to_id: nil, in_reply_to_account_id: nil, + card: nil, reblog: nil, content: HtmlSanitizeEx.basic_html(note.data["object"]["content"]), created_at: created_at, @@ -49,14 +98,21 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do replies_count: 0, favourites_count: 0, reblogged: false, + bookmarked: false, favourited: false, muted: false, + pinned: false, sensitive: false, - spoiler_text: note.data["object"]["summary"], + spoiler_text: HtmlSanitizeEx.basic_html(note.data["object"]["summary"]), visibility: "public", media_attachments: [], mentions: [], - tags: [], + tags: [ + %{ + name: "#{note.data["object"]["tag"]}", + url: "/tag/#{note.data["object"]["tag"]}" + } + ], application: %{ name: "Web", website: nil @@ -69,12 +125,34 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do static_url: "corndog.png", visible_in_picker: false } - ] + ], + pleroma: %{ + local: true, + conversation_id: convo_id, + content: %{"text/plain" => HtmlSanitizeEx.strip_tags(note.data["object"]["content"])}, + spoiler_text: %{"text/plain" => HtmlSanitizeEx.strip_tags(note.data["object"]["summary"])} + } } assert status == expected end + test "tells if the message is muted for some reason" do + user = insert(:user) + other_user = insert(:user) + + {:ok, user} = User.mute(user, other_user) + + {:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"}) + status = StatusView.render("status.json", %{activity: activity}) + + assert status.muted == false + + status = StatusView.render("status.json", %{activity: activity, for: user}) + + assert status.muted == true + end + test "a reply" do note = insert(:note_activity) user = insert(:user) @@ -101,7 +179,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do status = StatusView.render("status.json", %{activity: activity}) - assert status.mentions == [AccountView.render("mention.json", %{user: user})] + actor = User.get_by_ap_id(activity.actor) + + assert status.mentions == + Enum.map([user, actor], fn u -> AccountView.render("mention.json", %{user: u}) end) end test "attachments" do @@ -123,7 +204,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do remote_url: "someurl", preview_url: "someurl", text_url: "someurl", - description: nil + description: nil, + pleroma: %{mime_type: "image/png"} } assert expected == StatusView.render("attachment.json", %{attachment: object}) @@ -145,4 +227,96 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do assert represented[:reblog][:id] == to_string(activity.id) assert represented[:emojis] == [] end + + test "a peertube video" do + user = insert(:user) + + {:ok, object} = + ActivityPub.fetch_object_from_id( + "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" + ) + + %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"]) + + represented = StatusView.render("status.json", %{for: user, activity: activity}) + + assert represented[:id] == to_string(activity.id) + assert length(represented[:media_attachments]) == 1 + end + + describe "build_tags/1" do + test "it returns a a dictionary tags" do + object_tags = [ + "fediverse", + "mastodon", + "nextcloud", + %{ + "href" => "https://kawen.space/users/lain", + "name" => "@lain@kawen.space", + "type" => "Mention" + } + ] + + assert StatusView.build_tags(object_tags) == [ + %{name: "fediverse", url: "/tag/fediverse"}, + %{name: "mastodon", url: "/tag/mastodon"}, + %{name: "nextcloud", url: "/tag/nextcloud"} + ] + end + end + + describe "rich media cards" do + test "a rich media card without a site name renders correctly" do + page_url = "http://example.com" + + card = %{ + url: page_url, + image: page_url <> "/example.jpg", + title: "Example website" + } + + %{provider_name: "example.com"} = + StatusView.render("card.json", %{page_url: page_url, rich_media: card}) + end + + test "a rich media card without a site name or image renders correctly" do + page_url = "http://example.com" + + card = %{ + url: page_url, + title: "Example website" + } + + %{provider_name: "example.com"} = + StatusView.render("card.json", %{page_url: page_url, rich_media: card}) + end + + test "a rich media card without an image renders correctly" do + page_url = "http://example.com" + + card = %{ + url: page_url, + site_name: "Example site name", + title: "Example website" + } + + %{provider_name: "Example site name"} = + StatusView.render("card.json", %{page_url: page_url, rich_media: card}) + end + + test "a rich media card with all relevant data renders correctly" do + page_url = "http://example.com" + + card = %{ + url: page_url, + site_name: "Example site name", + title: "Example website", + image: page_url <> "/example.jpg", + description: "Example description" + } + + %{provider_name: "Example site name"} = + StatusView.render("card.json", %{page_url: page_url, rich_media: card}) + end + end end diff --git a/test/web/mastodon_api/subscription_controller_test.exs b/test/web/mastodon_api/subscription_controller_test.exs new file mode 100644 index 000000000..7dfb02f63 --- /dev/null +++ b/test/web/mastodon_api/subscription_controller_test.exs @@ -0,0 +1,192 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + alias Pleroma.Web.Push + alias Pleroma.Web.Push.Subscription + + @sub %{ + "endpoint" => "https://example.com/example/1234", + "keys" => %{ + "auth" => "8eDyX_uCN0XRhSbY5hs7Hg==", + "p256dh" => + "BCIWgsnyXDv1VkhqL2P7YRBvdeuDnlwAPT2guNhdIoW3IP7GmHh1SMKPLxRf7x8vJy6ZFK3ol2ohgn_-0yP7QQA=" + } + } + @server_key Keyword.get(Push.vapid_config(), :public_key) + + setup do + user = insert(:user) + token = insert(:oauth_token, user: user, scopes: ["push"]) + + conn = + build_conn() + |> assign(:user, user) + |> assign(:token, token) + + %{conn: conn, user: user, token: token} + end + + defmacro assert_error_when_disable_push(do: yield) do + quote do + vapid_details = Application.get_env(:web_push_encryption, :vapid_details, []) + Application.put_env(:web_push_encryption, :vapid_details, []) + assert "Something went wrong" == unquote(yield) + Application.put_env(:web_push_encryption, :vapid_details, vapid_details) + end + end + + describe "creates push subscription" do + test "returns error when push disabled ", %{conn: conn} do + assert_error_when_disable_push do + conn + |> post("/api/v1/push/subscription", %{}) + |> json_response(500) + end + end + + test "successful creation", %{conn: conn} do + result = + conn + |> post("/api/v1/push/subscription", %{ + "data" => %{"alerts" => %{"mention" => true, "test" => true}}, + "subscription" => @sub + }) + |> json_response(200) + + [subscription] = Pleroma.Repo.all(Subscription) + + assert %{ + "alerts" => %{"mention" => true}, + "endpoint" => subscription.endpoint, + "id" => to_string(subscription.id), + "server_key" => @server_key + } == result + end + end + + describe "gets a user subscription" do + test "returns error when push disabled ", %{conn: conn} do + assert_error_when_disable_push do + conn + |> get("/api/v1/push/subscription", %{}) + |> json_response(500) + end + end + + test "returns error when user hasn't subscription", %{conn: conn} do + res = + conn + |> get("/api/v1/push/subscription", %{}) + |> json_response(404) + + assert "Not found" == res + end + + test "returns a user subsciption", %{conn: conn, user: user, token: token} do + subscription = + insert(:push_subscription, + user: user, + token: token, + data: %{"alerts" => %{"mention" => true}} + ) + + res = + conn + |> get("/api/v1/push/subscription", %{}) + |> json_response(200) + + expect = %{ + "alerts" => %{"mention" => true}, + "endpoint" => "https://example.com/example/1234", + "id" => to_string(subscription.id), + "server_key" => @server_key + } + + assert expect == res + end + end + + describe "updates a user subsciption" do + setup %{conn: conn, user: user, token: token} do + subscription = + insert(:push_subscription, + user: user, + token: token, + data: %{"alerts" => %{"mention" => true}} + ) + + %{conn: conn, user: user, token: token, subscription: subscription} + end + + test "returns error when push disabled ", %{conn: conn} do + assert_error_when_disable_push do + conn + |> put("/api/v1/push/subscription", %{data: %{"alerts" => %{"mention" => false}}}) + |> json_response(500) + end + end + + test "returns updated subsciption", %{conn: conn, subscription: subscription} do + res = + conn + |> put("/api/v1/push/subscription", %{ + data: %{"alerts" => %{"mention" => false, "follow" => true}} + }) + |> json_response(200) + + expect = %{ + "alerts" => %{"follow" => true, "mention" => false}, + "endpoint" => "https://example.com/example/1234", + "id" => to_string(subscription.id), + "server_key" => @server_key + } + + assert expect == res + end + end + + describe "deletes the user subscription" do + test "returns error when push disabled ", %{conn: conn} do + assert_error_when_disable_push do + conn + |> delete("/api/v1/push/subscription", %{}) + |> json_response(500) + end + end + + test "returns error when user hasn't subscription", %{conn: conn} do + res = + conn + |> delete("/api/v1/push/subscription", %{}) + |> json_response(404) + + assert "Not found" == res + end + + test "returns empty result and delete user subsciption", %{ + conn: conn, + user: user, + token: token + } do + subscription = + insert(:push_subscription, + user: user, + token: token, + data: %{"alerts" => %{"mention" => true}} + ) + + res = + conn + |> delete("/api/v1/push/subscription", %{}) + |> json_response(200) + + assert %{} == res + refute Pleroma.Repo.get(Subscription, subscription.id) + end + end +end diff --git a/test/web/metadata/opengraph_test.exs b/test/web/metadata/opengraph_test.exs new file mode 100644 index 000000000..4283f72cd --- /dev/null +++ b/test/web/metadata/opengraph_test.exs @@ -0,0 +1,94 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Metadata.Providers.OpenGraphTest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Web.Metadata.Providers.OpenGraph + + test "it renders all supported types of attachments and skips unknown types" do + user = insert(:user) + + note = + insert(:note, %{ + data: %{ + "actor" => user.ap_id, + "tag" => [], + "id" => "https://pleroma.gov/objects/whatever", + "content" => "pleroma in a nutshell", + "attachment" => [ + %{ + "url" => [ + %{"mediaType" => "image/png", "href" => "https://pleroma.gov/tenshi.png"} + ] + }, + %{ + "url" => [ + %{ + "mediaType" => "application/octet-stream", + "href" => "https://pleroma.gov/fqa/badapple.sfc" + } + ] + }, + %{ + "url" => [ + %{"mediaType" => "video/webm", "href" => "https://pleroma.gov/about/juche.webm"} + ] + }, + %{ + "url" => [ + %{ + "mediaType" => "audio/basic", + "href" => "http://www.gnu.org/music/free-software-song.au" + } + ] + } + ] + } + }) + + result = OpenGraph.build_tags(%{object: note, url: note.data["id"], user: user}) + + assert Enum.all?( + [ + {:meta, [property: "og:image", content: "https://pleroma.gov/tenshi.png"], []}, + {:meta, + [property: "og:audio", content: "http://www.gnu.org/music/free-software-song.au"], + []}, + {:meta, [property: "og:video", content: "https://pleroma.gov/about/juche.webm"], + []} + ], + fn element -> element in result end + ) + end + + test "it does not render attachments if post is nsfw" do + Pleroma.Config.put([Pleroma.Web.Metadata, :unfurl_nsfw], false) + user = insert(:user, avatar: %{"url" => [%{"href" => "https://pleroma.gov/tenshi.png"}]}) + + note = + insert(:note, %{ + data: %{ + "actor" => user.ap_id, + "id" => "https://pleroma.gov/objects/whatever", + "content" => "#cuteposting #nsfw #hambaga", + "tag" => ["cuteposting", "nsfw", "hambaga"], + "sensitive" => true, + "attachment" => [ + %{ + "url" => [ + %{"mediaType" => "image/png", "href" => "https://misskey.microsoft/corndog.png"} + ] + } + ] + } + }) + + result = OpenGraph.build_tags(%{object: note, url: note.data["id"], user: user}) + + assert {:meta, [property: "og:image", content: "https://pleroma.gov/tenshi.png"], []} in result + + refute {:meta, [property: "og:image", content: "https://misskey.microsoft/corndog.png"], []} in result + end +end diff --git a/test/web/node_info_test.exs b/test/web/node_info_test.exs index a5b0b7869..2fc42b7cc 100644 --- a/test/web/node_info_test.exs +++ b/test/web/node_info_test.exs @@ -1,18 +1,35 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.NodeInfoTest do use Pleroma.Web.ConnCase import Pleroma.Factory test "nodeinfo shows staff accounts", %{conn: conn} do - user = insert(:user, %{local: true, info: %{is_moderator: true}}) + moderator = insert(:user, %{local: true, info: %{is_moderator: true}}) + admin = insert(:user, %{local: true, info: %{is_admin: true}}) + + conn = + conn + |> get("/nodeinfo/2.1.json") + + assert result = json_response(conn, 200) + + assert moderator.ap_id in result["metadata"]["staffAccounts"] + assert admin.ap_id in result["metadata"]["staffAccounts"] + end + test "nodeinfo shows restricted nicknames", %{conn: conn} do conn = conn - |> get("/nodeinfo/2.0.json") + |> get("/nodeinfo/2.1.json") assert result = json_response(conn, 200) - assert user.ap_id in result["metadata"]["staffAccounts"] + assert Pleroma.Config.get([Pleroma.User, :restricted_nicknames]) == + result["metadata"]["restrictedNicknames"] end test "returns 404 when federation is disabled", %{conn: conn} do @@ -27,7 +44,7 @@ defmodule Pleroma.Web.NodeInfoTest do |> json_response(404) conn - |> get("/nodeinfo/2.0.json") + |> get("/nodeinfo/2.1.json") |> json_response(404) instance = @@ -43,7 +60,75 @@ defmodule Pleroma.Web.NodeInfoTest do |> json_response(200) conn + |> get("/nodeinfo/2.1.json") + |> json_response(200) + end + + test "returns 404 when federation is disabled (nodeinfo 2.0)", %{conn: conn} do + instance = + Application.get_env(:pleroma, :instance) + |> Keyword.put(:federating, false) + + Application.put_env(:pleroma, :instance, instance) + + conn + |> get("/.well-known/nodeinfo") + |> json_response(404) + + conn + |> get("/nodeinfo/2.0.json") + |> json_response(404) + + instance = + Application.get_env(:pleroma, :instance) + |> Keyword.put(:federating, true) + + Application.put_env(:pleroma, :instance, instance) + end + + test "returns 200 when federation is enabled (nodeinfo 2.0)", %{conn: conn} do + conn + |> get("/.well-known/nodeinfo") + |> json_response(200) + + conn |> get("/nodeinfo/2.0.json") |> json_response(200) end + + test "returns software.repository field in nodeinfo 2.1", %{conn: conn} do + conn + |> get("/.well-known/nodeinfo") + |> json_response(200) + + conn = + conn + |> get("/nodeinfo/2.1.json") + + assert result = json_response(conn, 200) + assert Pleroma.Application.repository() == result["software"]["repository"] + end + + test "it returns the safe_dm_mentions feature if enabled", %{conn: conn} do + option = Pleroma.Config.get([:instance, :safe_dm_mentions]) + Pleroma.Config.put([:instance, :safe_dm_mentions], true) + + response = + conn + |> get("/nodeinfo/2.1.json") + |> json_response(:ok) + + assert "safe_dm_mentions" in response["metadata"]["features"] + + Pleroma.Config.put([:instance, :safe_dm_mentions], false) + + response = + conn + |> get("/nodeinfo/2.1.json") + |> json_response(:ok) + + refute "safe_dm_mentions" in response["metadata"]["features"] + + Pleroma.Config.put([:instance, :safe_dm_mentions], option) + end end diff --git a/test/web/oauth/authorization_test.exs b/test/web/oauth/authorization_test.exs index 98c7c4133..d8b008437 100644 --- a/test/web/oauth/authorization_test.exs +++ b/test/web/oauth/authorization_test.exs @@ -1,38 +1,44 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.OAuth.AuthorizationTest do use Pleroma.DataCase - alias Pleroma.Web.OAuth.{Authorization, App} + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OAuth.Authorization import Pleroma.Factory - test "create an authorization token for a valid app" do + setup do {:ok, app} = Repo.insert( App.register_changeset(%App{}, %{ client_name: "client", - scopes: "scope", + scopes: ["read", "write"], redirect_uris: "url" }) ) + %{app: app} + end + + test "create an authorization token for a valid app", %{app: app} do user = insert(:user) - {:ok, auth} = Authorization.create_authorization(app, user) + {:ok, auth1} = Authorization.create_authorization(app, user) + assert auth1.scopes == app.scopes - assert auth.user_id == user.id - assert auth.app_id == app.id - assert String.length(auth.token) > 10 - assert auth.used == false - end + {:ok, auth2} = Authorization.create_authorization(app, user, ["read"]) + assert auth2.scopes == ["read"] - test "use up a token" do - {:ok, app} = - Repo.insert( - App.register_changeset(%App{}, %{ - client_name: "client", - scopes: "scope", - redirect_uris: "url" - }) - ) + for auth <- [auth1, auth2] do + assert auth.user_id == user.id + assert auth.app_id == app.id + assert String.length(auth.token) > 10 + assert auth.used == false + end + end + test "use up a token", %{app: app} do user = insert(:user) {:ok, auth} = Authorization.create_authorization(app, user) @@ -56,22 +62,13 @@ defmodule Pleroma.Web.OAuth.AuthorizationTest do assert {:error, "token expired"} == Authorization.use_token(expired_auth) end - test "delete authorizations" do - {:ok, app} = - Repo.insert( - App.register_changeset(%App{}, %{ - client_name: "client", - scopes: "scope", - redirect_uris: "url" - }) - ) - + test "delete authorizations", %{app: app} do user = insert(:user) {:ok, auth} = Authorization.create_authorization(app, user) {:ok, auth} = Authorization.use_token(auth) - {auths, _} = Authorization.delete_user_authorizations(user) + Authorization.delete_user_authorizations(user) {_, invalid} = Authorization.use_token(auth) diff --git a/test/web/oauth/ldap_authorization_test.exs b/test/web/oauth/ldap_authorization_test.exs new file mode 100644 index 000000000..0eb191c76 --- /dev/null +++ b/test/web/oauth/ldap_authorization_test.exs @@ -0,0 +1,195 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.LDAPAuthorizationTest do + use Pleroma.Web.ConnCase + alias Pleroma.Repo + alias Pleroma.Web.OAuth.Token + import Pleroma.Factory + import ExUnit.CaptureLog + import Mock + + @skip if !Code.ensure_loaded?(:eldap), do: :skip + + setup_all do + ldap_authenticator = + Pleroma.Config.get(Pleroma.Web.Auth.Authenticator, Pleroma.Web.Auth.PleromaAuthenticator) + + ldap_enabled = Pleroma.Config.get([:ldap, :enabled]) + + on_exit(fn -> + Pleroma.Config.put(Pleroma.Web.Auth.Authenticator, ldap_authenticator) + Pleroma.Config.put([:ldap, :enabled], ldap_enabled) + end) + + Pleroma.Config.put(Pleroma.Web.Auth.Authenticator, Pleroma.Web.Auth.LDAPAuthenticator) + Pleroma.Config.put([:ldap, :enabled], true) + + :ok + end + + @tag @skip + test "authorizes the existing user using LDAP credentials" do + password = "testpassword" + user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password)) + app = insert(:oauth_app, scopes: ["read", "write"]) + + host = Pleroma.Config.get([:ldap, :host]) |> to_charlist + port = Pleroma.Config.get([:ldap, :port]) + + with_mocks [ + {:eldap, [], + [ + open: fn [^host], [{:port, ^port}, {:ssl, false} | _] -> {:ok, self()} end, + simple_bind: fn _connection, _dn, ^password -> :ok end, + close: fn _connection -> + send(self(), :close_connection) + :ok + end + ]} + ] do + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert %{"access_token" => token} = json_response(conn, 200) + + token = Repo.get_by(Token, token: token) + + assert token.user_id == user.id + assert_received :close_connection + end + end + + @tag @skip + test "creates a new user after successful LDAP authorization" do + password = "testpassword" + user = build(:user) + app = insert(:oauth_app, scopes: ["read", "write"]) + + host = Pleroma.Config.get([:ldap, :host]) |> to_charlist + port = Pleroma.Config.get([:ldap, :port]) + + with_mocks [ + {:eldap, [], + [ + open: fn [^host], [{:port, ^port}, {:ssl, false} | _] -> {:ok, self()} end, + simple_bind: fn _connection, _dn, ^password -> :ok end, + equalityMatch: fn _type, _value -> :ok end, + wholeSubtree: fn -> :ok end, + search: fn _connection, _options -> + {:ok, + {:eldap_search_result, [{:eldap_entry, '', [{'mail', [to_charlist(user.email)]}]}], + []}} + end, + close: fn _connection -> + send(self(), :close_connection) + :ok + end + ]} + ] do + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert %{"access_token" => token} = json_response(conn, 200) + + token = Repo.get_by(Token, token: token) |> Repo.preload(:user) + + assert token.user.nickname == user.nickname + assert_received :close_connection + end + end + + @tag @skip + test "falls back to the default authorization when LDAP is unavailable" do + password = "testpassword" + user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password)) + app = insert(:oauth_app, scopes: ["read", "write"]) + + host = Pleroma.Config.get([:ldap, :host]) |> to_charlist + port = Pleroma.Config.get([:ldap, :port]) + + with_mocks [ + {:eldap, [], + [ + open: fn [^host], [{:port, ^port}, {:ssl, false} | _] -> {:error, 'connect failed'} end, + simple_bind: fn _connection, _dn, ^password -> :ok end, + close: fn _connection -> + send(self(), :close_connection) + :ok + end + ]} + ] do + log = + capture_log(fn -> + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert %{"access_token" => token} = json_response(conn, 200) + + token = Repo.get_by(Token, token: token) + + assert token.user_id == user.id + end) + + assert log =~ "Could not open LDAP connection: 'connect failed'" + refute_received :close_connection + end + end + + @tag @skip + test "disallow authorization for wrong LDAP credentials" do + password = "testpassword" + user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password)) + app = insert(:oauth_app, scopes: ["read", "write"]) + + host = Pleroma.Config.get([:ldap, :host]) |> to_charlist + port = Pleroma.Config.get([:ldap, :port]) + + with_mocks [ + {:eldap, [], + [ + open: fn [^host], [{:port, ^port}, {:ssl, false} | _] -> {:ok, self()} end, + simple_bind: fn _connection, _dn, ^password -> {:error, :invalidCredentials} end, + close: fn _connection -> + send(self(), :close_connection) + :ok + end + ]} + ] do + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert %{"error" => "Invalid credentials"} = json_response(conn, 400) + assert_received :close_connection + end + end +end diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs index 3a902f128..ac7843f9b 100644 --- a/test/web/oauth/oauth_controller_test.exs +++ b/test/web/oauth/oauth_controller_test.exs @@ -1,113 +1,680 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.OAuth.OAuthControllerTest do use Pleroma.Web.ConnCase import Pleroma.Factory + import Mock + alias Pleroma.Registration alias Pleroma.Repo - alias Pleroma.Web.OAuth.{Authorization, Token} - - test "redirects with oauth authorization" do - user = insert(:user) - app = insert(:oauth_app) - - conn = - build_conn() - |> post("/oauth/authorize", %{ - "authorization" => %{ - "name" => user.nickname, - "password" => "test", - "client_id" => app.client_id, - "redirect_uri" => app.redirect_uris, - "state" => "statepassed" - } - }) + alias Pleroma.Web.OAuth.Authorization + alias Pleroma.Web.OAuth.Token - target = redirected_to(conn) - assert target =~ app.redirect_uris + @session_opts [ + store: :cookie, + key: "_test", + signing_salt: "cooldude" + ] - query = URI.parse(target).query |> URI.query_decoder() |> Map.new() + describe "in OAuth consumer mode, " do + setup do + oauth_consumer_strategies_path = [:auth, :oauth_consumer_strategies] + oauth_consumer_strategies = Pleroma.Config.get(oauth_consumer_strategies_path) + Pleroma.Config.put(oauth_consumer_strategies_path, ~w(twitter facebook)) - assert %{"state" => "statepassed", "code" => code} = query - assert Repo.get_by(Authorization, token: code) - end + on_exit(fn -> + Pleroma.Config.put(oauth_consumer_strategies_path, oauth_consumer_strategies) + end) + + [ + app: insert(:oauth_app), + conn: + build_conn() + |> Plug.Session.call(Plug.Session.init(@session_opts)) + |> fetch_session() + ] + end + + test "GET /oauth/authorize renders auth forms, including OAuth consumer form", %{ + app: app, + conn: conn + } do + conn = + get( + conn, + "/oauth/authorize", + %{ + "response_type" => "code", + "client_id" => app.client_id, + "redirect_uri" => app.redirect_uris, + "scope" => "read" + } + ) + + assert response = html_response(conn, 200) + assert response =~ "Sign in with Twitter" + assert response =~ o_auth_path(conn, :prepare_request) + end + + test "GET /oauth/prepare_request encodes parameters as `state` and redirects", %{ + app: app, + conn: conn + } do + conn = + get( + conn, + "/oauth/prepare_request", + %{ + "provider" => "twitter", + "scope" => "read follow", + "client_id" => app.client_id, + "redirect_uri" => app.redirect_uris, + "state" => "a_state" + } + ) + + assert response = html_response(conn, 302) + + redirect_query = URI.parse(redirected_to(conn)).query + assert %{"state" => state_param} = URI.decode_query(redirect_query) + assert {:ok, state_components} = Poison.decode(state_param) + + expected_client_id = app.client_id + expected_redirect_uri = app.redirect_uris + + assert %{ + "scope" => "read follow", + "client_id" => ^expected_client_id, + "redirect_uri" => ^expected_redirect_uri, + "state" => "a_state" + } = state_components + end + + test "with user-bound registration, GET /oauth/<provider>/callback redirects to `redirect_uri` with `code`", + %{app: app, conn: conn} do + registration = insert(:registration) + + state_params = %{ + "scope" => Enum.join(app.scopes, " "), + "client_id" => app.client_id, + "redirect_uri" => app.redirect_uris, + "state" => "" + } + + with_mock Pleroma.Web.Auth.Authenticator, + get_registration: fn _, _ -> {:ok, registration} end do + conn = + get( + conn, + "/oauth/twitter/callback", + %{ + "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", + "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", + "provider" => "twitter", + "state" => Poison.encode!(state_params) + } + ) + + assert response = html_response(conn, 302) + assert redirected_to(conn) =~ ~r/#{app.redirect_uris}\?code=.+/ + end + end + + test "with user-unbound registration, GET /oauth/<provider>/callback renders registration_details page", + %{app: app, conn: conn} do + registration = insert(:registration, user: nil) + + state_params = %{ + "scope" => "read write", + "client_id" => app.client_id, + "redirect_uri" => app.redirect_uris, + "state" => "a_state" + } + + with_mock Pleroma.Web.Auth.Authenticator, + get_registration: fn _, _ -> {:ok, registration} end do + conn = + get( + conn, + "/oauth/twitter/callback", + %{ + "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", + "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", + "provider" => "twitter", + "state" => Poison.encode!(state_params) + } + ) + + assert response = html_response(conn, 200) + assert response =~ ~r/name="op" type="submit" value="register"/ + assert response =~ ~r/name="op" type="submit" value="connect"/ + assert response =~ Registration.email(registration) + assert response =~ Registration.nickname(registration) + end + end + + test "on authentication error, GET /oauth/<provider>/callback redirects to `redirect_uri`", %{ + app: app, + conn: conn + } do + state_params = %{ + "scope" => Enum.join(app.scopes, " "), + "client_id" => app.client_id, + "redirect_uri" => app.redirect_uris, + "state" => "" + } + + conn = + conn + |> assign(:ueberauth_failure, %{errors: [%{message: "(error description)"}]}) + |> get( + "/oauth/twitter/callback", + %{ + "oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM", + "oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs", + "provider" => "twitter", + "state" => Poison.encode!(state_params) + } + ) + + assert response = html_response(conn, 302) + assert redirected_to(conn) == app.redirect_uris + assert get_flash(conn, :error) == "Failed to authenticate: (error description)." + end + + test "GET /oauth/registration_details renders registration details form", %{ + app: app, + conn: conn + } do + conn = + get( + conn, + "/oauth/registration_details", + %{ + "scopes" => app.scopes, + "client_id" => app.client_id, + "redirect_uri" => app.redirect_uris, + "state" => "a_state", + "nickname" => nil, + "email" => "john@doe.com" + } + ) + + assert response = html_response(conn, 200) + assert response =~ ~r/name="op" type="submit" value="register"/ + assert response =~ ~r/name="op" type="submit" value="connect"/ + end + + test "with valid params, POST /oauth/register?op=register redirects to `redirect_uri` with `code`", + %{ + app: app, + conn: conn + } do + registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil}) + + conn = + conn + |> put_session(:registration_id, registration.id) + |> post( + "/oauth/register", + %{ + "op" => "register", + "scopes" => app.scopes, + "client_id" => app.client_id, + "redirect_uri" => app.redirect_uris, + "state" => "a_state", + "nickname" => "availablenick", + "email" => "available@email.com" + } + ) - test "issues a token for an all-body request" do - user = insert(:user) - app = insert(:oauth_app) + assert response = html_response(conn, 302) + assert redirected_to(conn) =~ ~r/#{app.redirect_uris}\?code=.+/ + end - {:ok, auth} = Authorization.create_authorization(app, user) + test "with invalid params, POST /oauth/register?op=register renders registration_details page", + %{ + app: app, + conn: conn + } do + another_user = insert(:user) + registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil}) - conn = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "authorization_code", - "code" => auth.token, + params = %{ + "op" => "register", + "scopes" => app.scopes, + "client_id" => app.client_id, "redirect_uri" => app.redirect_uris, + "state" => "a_state", + "nickname" => "availablenickname", + "email" => "available@email.com" + } + + for {bad_param, bad_param_value} <- + [{"nickname", another_user.nickname}, {"email", another_user.email}] do + bad_params = Map.put(params, bad_param, bad_param_value) + + conn = + conn + |> put_session(:registration_id, registration.id) + |> post("/oauth/register", bad_params) + + assert html_response(conn, 403) =~ ~r/name="op" type="submit" value="register"/ + assert get_flash(conn, :error) == "Error: #{bad_param} has already been taken." + end + end + + test "with valid params, POST /oauth/register?op=connect redirects to `redirect_uri` with `code`", + %{ + app: app, + conn: conn + } do + user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt("testpassword")) + registration = insert(:registration, user: nil) + + conn = + conn + |> put_session(:registration_id, registration.id) + |> post( + "/oauth/register", + %{ + "op" => "connect", + "scopes" => app.scopes, + "client_id" => app.client_id, + "redirect_uri" => app.redirect_uris, + "state" => "a_state", + "auth_name" => user.nickname, + "password" => "testpassword" + } + ) + + assert response = html_response(conn, 302) + assert redirected_to(conn) =~ ~r/#{app.redirect_uris}\?code=.+/ + end + + test "with invalid params, POST /oauth/register?op=connect renders registration_details page", + %{ + app: app, + conn: conn + } do + user = insert(:user) + registration = insert(:registration, user: nil) + + params = %{ + "op" => "connect", + "scopes" => app.scopes, "client_id" => app.client_id, - "client_secret" => app.client_secret - }) + "redirect_uri" => app.redirect_uris, + "state" => "a_state", + "auth_name" => user.nickname, + "password" => "wrong password" + } - assert %{"access_token" => token} = json_response(conn, 200) - assert Repo.get_by(Token, token: token) + conn = + conn + |> put_session(:registration_id, registration.id) + |> post("/oauth/register", params) + + assert html_response(conn, 401) =~ ~r/name="op" type="submit" value="connect"/ + assert get_flash(conn, :error) == "Invalid Username/Password" + end end - test "issues a token for request with HTTP basic auth client credentials" do - user = insert(:user) - app = insert(:oauth_app) + describe "GET /oauth/authorize" do + setup do + [ + app: insert(:oauth_app, redirect_uris: "https://redirect.url"), + conn: + build_conn() + |> Plug.Session.call(Plug.Session.init(@session_opts)) + |> fetch_session() + ] + end + + test "renders authentication page", %{app: app, conn: conn} do + conn = + get( + conn, + "/oauth/authorize", + %{ + "response_type" => "code", + "client_id" => app.client_id, + "redirect_uri" => app.redirect_uris, + "scope" => "read" + } + ) + + assert html_response(conn, 200) =~ ~s(type="submit") + end - {:ok, auth} = Authorization.create_authorization(app, user) + test "renders authentication page if user is already authenticated but `force_login` is tru-ish", + %{app: app, conn: conn} do + token = insert(:oauth_token, app_id: app.id) - app_encoded = - (URI.encode_www_form(app.client_id) <> ":" <> URI.encode_www_form(app.client_secret)) - |> Base.encode64() + conn = + conn + |> put_session(:oauth_token, token.token) + |> get( + "/oauth/authorize", + %{ + "response_type" => "code", + "client_id" => app.client_id, + "redirect_uri" => app.redirect_uris, + "scope" => "read", + "force_login" => "true" + } + ) - conn = - build_conn() - |> put_req_header("authorization", "Basic " <> app_encoded) - |> post("/oauth/token", %{ - "grant_type" => "authorization_code", - "code" => auth.token, - "redirect_uri" => app.redirect_uris - }) + assert html_response(conn, 200) =~ ~s(type="submit") + end - assert %{"access_token" => token} = json_response(conn, 200) - assert Repo.get_by(Token, token: token) + test "redirects to app if user is already authenticated", %{app: app, conn: conn} do + token = insert(:oauth_token, app_id: app.id) + + conn = + conn + |> put_session(:oauth_token, token.token) + |> get( + "/oauth/authorize", + %{ + "response_type" => "code", + "client_id" => app.client_id, + "redirect_uri" => app.redirect_uris, + "scope" => "read" + } + ) + + assert redirected_to(conn) == "https://redirect.url" + end end - test "rejects token exchange with invalid client credentials" do - user = insert(:user) - app = insert(:oauth_app) + describe "POST /oauth/authorize" do + test "redirects with oauth authorization" do + user = insert(:user) + app = insert(:oauth_app, scopes: ["read", "write", "follow"]) + + conn = + build_conn() + |> post("/oauth/authorize", %{ + "authorization" => %{ + "name" => user.nickname, + "password" => "test", + "client_id" => app.client_id, + "redirect_uri" => app.redirect_uris, + "scope" => "read write", + "state" => "statepassed" + } + }) + + target = redirected_to(conn) + assert target =~ app.redirect_uris + + query = URI.parse(target).query |> URI.query_decoder() |> Map.new() + + assert %{"state" => "statepassed", "code" => code} = query + auth = Repo.get_by(Authorization, token: code) + assert auth + assert auth.scopes == ["read", "write"] + end - {:ok, auth} = Authorization.create_authorization(app, user) + test "returns 401 for wrong credentials", %{conn: conn} do + user = insert(:user) + app = insert(:oauth_app) - conn = - build_conn() - |> put_req_header("authorization", "Basic JTIxOiVGMCU5RiVBNCVCNwo=") - |> post("/oauth/token", %{ - "grant_type" => "authorization_code", - "code" => auth.token, - "redirect_uri" => app.redirect_uris - }) + result = + conn + |> post("/oauth/authorize", %{ + "authorization" => %{ + "name" => user.nickname, + "password" => "wrong", + "client_id" => app.client_id, + "redirect_uri" => app.redirect_uris, + "state" => "statepassed", + "scope" => Enum.join(app.scopes, " ") + } + }) + |> html_response(:unauthorized) - assert resp = json_response(conn, 400) - assert %{"error" => _} = resp - refute Map.has_key?(resp, "access_token") + # Keep the details + assert result =~ app.client_id + assert result =~ app.redirect_uris + + # Error message + assert result =~ "Invalid Username/Password" + end + + test "returns 401 for missing scopes", %{conn: conn} do + user = insert(:user) + app = insert(:oauth_app) + + result = + conn + |> post("/oauth/authorize", %{ + "authorization" => %{ + "name" => user.nickname, + "password" => "test", + "client_id" => app.client_id, + "redirect_uri" => app.redirect_uris, + "state" => "statepassed", + "scope" => "" + } + }) + |> html_response(:unauthorized) + + # Keep the details + assert result =~ app.client_id + assert result =~ app.redirect_uris + + # Error message + assert result =~ "This action is outside the authorized scopes" + end + + test "returns 401 for scopes beyond app scopes", %{conn: conn} do + user = insert(:user) + app = insert(:oauth_app, scopes: ["read", "write"]) + + result = + conn + |> post("/oauth/authorize", %{ + "authorization" => %{ + "name" => user.nickname, + "password" => "test", + "client_id" => app.client_id, + "redirect_uri" => app.redirect_uris, + "state" => "statepassed", + "scope" => "read write follow" + } + }) + |> html_response(:unauthorized) + + # Keep the details + assert result =~ app.client_id + assert result =~ app.redirect_uris + + # Error message + assert result =~ "This action is outside the authorized scopes" + end end - test "rejects an invalid authorization code" do - app = insert(:oauth_app) + describe "POST /oauth/token" do + test "issues a token for an all-body request" do + user = insert(:user) + app = insert(:oauth_app, scopes: ["read", "write"]) - conn = - build_conn() - |> post("/oauth/token", %{ - "grant_type" => "authorization_code", - "code" => "Imobviouslyinvalid", - "redirect_uri" => app.redirect_uris, - "client_id" => app.client_id, - "client_secret" => app.client_secret - }) + {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) + + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "authorization_code", + "code" => auth.token, + "redirect_uri" => app.redirect_uris, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert %{"access_token" => token, "me" => ap_id} = json_response(conn, 200) + + token = Repo.get_by(Token, token: token) + assert token + assert token.scopes == auth.scopes + assert user.ap_id == ap_id + end + + test "issues a token for `password` grant_type with valid credentials, with full permissions by default" do + password = "testpassword" + user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password)) + + app = insert(:oauth_app, scopes: ["read", "write"]) + + # Note: "scope" param is intentionally omitted + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert %{"access_token" => token} = json_response(conn, 200) + + token = Repo.get_by(Token, token: token) + assert token + assert token.scopes == app.scopes + end + + test "issues a token for request with HTTP basic auth client credentials" do + user = insert(:user) + app = insert(:oauth_app, scopes: ["scope1", "scope2", "scope3"]) + + {:ok, auth} = Authorization.create_authorization(app, user, ["scope1", "scope2"]) + assert auth.scopes == ["scope1", "scope2"] + + app_encoded = + (URI.encode_www_form(app.client_id) <> ":" <> URI.encode_www_form(app.client_secret)) + |> Base.encode64() + + conn = + build_conn() + |> put_req_header("authorization", "Basic " <> app_encoded) + |> post("/oauth/token", %{ + "grant_type" => "authorization_code", + "code" => auth.token, + "redirect_uri" => app.redirect_uris + }) + + assert %{"access_token" => token, "scope" => scope} = json_response(conn, 200) + + assert scope == "scope1 scope2" + + token = Repo.get_by(Token, token: token) + assert token + assert token.scopes == ["scope1", "scope2"] + end + + test "rejects token exchange with invalid client credentials" do + user = insert(:user) + app = insert(:oauth_app) + + {:ok, auth} = Authorization.create_authorization(app, user) + + conn = + build_conn() + |> put_req_header("authorization", "Basic JTIxOiVGMCU5RiVBNCVCNwo=") + |> post("/oauth/token", %{ + "grant_type" => "authorization_code", + "code" => auth.token, + "redirect_uri" => app.redirect_uris + }) + + assert resp = json_response(conn, 400) + assert %{"error" => _} = resp + refute Map.has_key?(resp, "access_token") + end + + test "rejects token exchange for valid credentials belonging to unconfirmed user and confirmation is required" do + setting = Pleroma.Config.get([:instance, :account_activation_required]) + + unless setting do + Pleroma.Config.put([:instance, :account_activation_required], true) + on_exit(fn -> Pleroma.Config.put([:instance, :account_activation_required], setting) end) + end + + password = "testpassword" + user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password)) + info_change = Pleroma.User.Info.confirmation_changeset(user.info, :unconfirmed) + + {:ok, user} = + user + |> Ecto.Changeset.change() + |> Ecto.Changeset.put_embed(:info, info_change) + |> Repo.update() + + refute Pleroma.User.auth_active?(user) + + app = insert(:oauth_app) + + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert resp = json_response(conn, 403) + assert %{"error" => _} = resp + refute Map.has_key?(resp, "access_token") + end + + test "rejects token exchange for valid credentials belonging to deactivated user" do + password = "testpassword" + + user = + insert(:user, + password_hash: Comeonin.Pbkdf2.hashpwsalt(password), + info: %{deactivated: true} + ) + + app = insert(:oauth_app) + + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert resp = json_response(conn, 403) + assert %{"error" => _} = resp + refute Map.has_key?(resp, "access_token") + end + + test "rejects an invalid authorization code" do + app = insert(:oauth_app) + + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "authorization_code", + "code" => "Imobviouslyinvalid", + "redirect_uri" => app.redirect_uris, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) - assert resp = json_response(conn, 400) - assert %{"error" => _} = json_response(conn, 400) - refute Map.has_key?(resp, "access_token") + assert resp = json_response(conn, 400) + assert %{"error" => _} = json_response(conn, 400) + refute Map.has_key?(resp, "access_token") + end end end diff --git a/test/web/oauth/token_test.exs b/test/web/oauth/token_test.exs index f926ff50b..ad2a49f09 100644 --- a/test/web/oauth/token_test.exs +++ b/test/web/oauth/token_test.exs @@ -1,28 +1,36 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.OAuth.TokenTest do use Pleroma.DataCase - alias Pleroma.Web.OAuth.{App, Token, Authorization} alias Pleroma.Repo + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OAuth.Authorization + alias Pleroma.Web.OAuth.Token import Pleroma.Factory - test "exchanges a auth token for an access token" do + test "exchanges a auth token for an access token, preserving `scopes`" do {:ok, app} = Repo.insert( App.register_changeset(%App{}, %{ client_name: "client", - scopes: "scope", + scopes: ["read", "write"], redirect_uris: "url" }) ) user = insert(:user) - {:ok, auth} = Authorization.create_authorization(app, user) + {:ok, auth} = Authorization.create_authorization(app, user, ["read"]) + assert auth.scopes == ["read"] {:ok, token} = Token.exchange_token(app, auth) assert token.app_id == app.id assert token.user_id == user.id + assert token.scopes == auth.scopes assert String.length(token.token) > 10 assert String.length(token.refresh_token) > 10 @@ -35,7 +43,7 @@ defmodule Pleroma.Web.OAuth.TokenTest do Repo.insert( App.register_changeset(%App{}, %{ client_name: "client1", - scopes: "scope", + scopes: ["scope"], redirect_uris: "url" }) ) @@ -44,7 +52,7 @@ defmodule Pleroma.Web.OAuth.TokenTest do Repo.insert( App.register_changeset(%App{}, %{ client_name: "client2", - scopes: "scope", + scopes: ["scope"], redirect_uris: "url" }) ) @@ -54,8 +62,8 @@ defmodule Pleroma.Web.OAuth.TokenTest do {:ok, auth1} = Authorization.create_authorization(app1, user) {:ok, auth2} = Authorization.create_authorization(app2, user) - {:ok, token1} = Token.exchange_token(app1, auth1) - {:ok, token2} = Token.exchange_token(app2, auth2) + {:ok, _token1} = Token.exchange_token(app1, auth1) + {:ok, _token2} = Token.exchange_token(app2, auth2) {tokens, _} = Token.delete_user_tokens(user) diff --git a/test/web/ostatus/activity_representer_test.exs b/test/web/ostatus/activity_representer_test.exs index 8bf3bc775..a4bb68c4d 100644 --- a/test/web/ostatus/activity_representer_test.exs +++ b/test/web/ostatus/activity_representer_test.exs @@ -1,12 +1,24 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.OStatus.ActivityRepresenterTest do use Pleroma.DataCase - alias Pleroma.Web.OStatus.ActivityRepresenter - alias Pleroma.{User, Activity, Object} + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.OStatus + alias Pleroma.Web.OStatus.ActivityRepresenter import Pleroma.Factory + import Tesla.Mock + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end test "an external note activity" do incoming = File.read!("test/fixtures/mastodon-note-cw.xml") @@ -104,10 +116,10 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenterTest do {:ok, announce, _object} = ActivityPub.announce(user, object) - announce = Repo.get(Activity, announce.id) + announce = Activity.get_by_id(announce.id) note_user = User.get_cached_by_ap_id(note.data["actor"]) - note = Repo.get(Activity, note.id) + note = Activity.get_by_id(note.id) note_xml = ActivityRepresenter.to_simple_form(note, note_user, true) diff --git a/test/web/ostatus/feed_representer_test.exs b/test/web/ostatus/feed_representer_test.exs index bf3feb14e..3c7b126e7 100644 --- a/test/web/ostatus/feed_representer_test.exs +++ b/test/web/ostatus/feed_representer_test.exs @@ -1,9 +1,15 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.OStatus.FeedRepresenterTest do use Pleroma.DataCase import Pleroma.Factory alias Pleroma.User - alias Pleroma.Web.OStatus.{FeedRepresenter, UserRepresenter, ActivityRepresenter} alias Pleroma.Web.OStatus + alias Pleroma.Web.OStatus.ActivityRepresenter + alias Pleroma.Web.OStatus.FeedRepresenter + alias Pleroma.Web.OStatus.UserRepresenter test "returns a feed of the last 20 items of the user" do note_activity = insert(:note_activity) diff --git a/test/web/ostatus/incoming_documents/delete_handling_test.exs b/test/web/ostatus/incoming_documents/delete_handling_test.exs index 1e041e5b0..ca6e61339 100644 --- a/test/web/ostatus/incoming_documents/delete_handling_test.exs +++ b/test/web/ostatus/incoming_documents/delete_handling_test.exs @@ -2,9 +2,17 @@ defmodule Pleroma.Web.OStatus.DeleteHandlingTest do use Pleroma.DataCase import Pleroma.Factory - alias Pleroma.{Repo, Activity, Object} + import Tesla.Mock + + alias Pleroma.Activity + alias Pleroma.Object alias Pleroma.Web.OStatus + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + describe "deletions" do test "it removes the mentioned activity" do note = insert(:note_activity) @@ -23,10 +31,10 @@ defmodule Pleroma.Web.OStatus.DeleteHandlingTest do {:ok, [delete]} = OStatus.handle_incoming(incoming) - refute Repo.get(Activity, note.id) - refute Repo.get(Activity, like.id) - refute Object.get_by_ap_id(note.data["object"]["id"]) - assert Repo.get(Activity, second_note.id) + refute Activity.get_by_id(note.id) + refute Activity.get_by_id(like.id) + assert Object.get_by_ap_id(note.data["object"]["id"]).data["type"] == "Tombstone" + assert Activity.get_by_id(second_note.id) assert Object.get_by_ap_id(second_note.data["object"]["id"]) assert delete.data["type"] == "Delete" diff --git a/test/web/ostatus/ostatus_controller_test.exs b/test/web/ostatus/ostatus_controller_test.exs index e81adde68..2950f11c0 100644 --- a/test/web/ostatus/ostatus_controller_test.exs +++ b/test/web/ostatus/ostatus_controller_test.exs @@ -1,53 +1,66 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.OStatus.OStatusControllerTest do use Pleroma.Web.ConnCase import Pleroma.Factory - alias Pleroma.{User, Repo} + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.User alias Pleroma.Web.CommonAPI alias Pleroma.Web.OStatus.ActivityRepresenter - test "decodes a salmon", %{conn: conn} do - user = insert(:user) - salmon = File.read!("test/fixtures/salmon.xml") + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end - conn = - conn - |> put_req_header("content-type", "application/atom+xml") - |> post("/users/#{user.nickname}/salmon", salmon) + describe "salmon_incoming" do + test "decodes a salmon", %{conn: conn} do + user = insert(:user) + salmon = File.read!("test/fixtures/salmon.xml") - assert response(conn, 200) - end + conn = + conn + |> put_req_header("content-type", "application/atom+xml") + |> post("/users/#{user.nickname}/salmon", salmon) - test "decodes a salmon with a changed magic key", %{conn: conn} do - user = insert(:user) - salmon = File.read!("test/fixtures/salmon.xml") + assert response(conn, 200) + end - conn = - conn - |> put_req_header("content-type", "application/atom+xml") - |> post("/users/#{user.nickname}/salmon", salmon) + test "decodes a salmon with a changed magic key", %{conn: conn} do + user = insert(:user) + salmon = File.read!("test/fixtures/salmon.xml") + + conn = + conn + |> put_req_header("content-type", "application/atom+xml") + |> post("/users/#{user.nickname}/salmon", salmon) - assert response(conn, 200) + assert response(conn, 200) - # Set a wrong magic-key for a user so it has to refetch - salmon_user = User.get_by_ap_id("http://gs.example.org:4040/index.php/user/1") - # Wrong key - info_cng = - User.Info.remote_user_creation(salmon_user.info, %{ - magic_key: - "RSA.pu0s-halox4tu7wmES1FVSx6u-4wc0YrUFXcqWXZG4-27UmbCOpMQftRCldNRfyA-qLbz-eqiwrong1EwUvjsD4cYbAHNGHwTvDOyx5AKthQUP44ykPv7kjKGh3DWKySJvcs9tlUG87hlo7AvnMo9pwRS_Zz2CacQ-MKaXyDepk=.AQAB" - }) + # Set a wrong magic-key for a user so it has to refetch + salmon_user = User.get_by_ap_id("http://gs.example.org:4040/index.php/user/1") + # Wrong key + info_cng = + User.Info.remote_user_creation(salmon_user.info, %{ + magic_key: + "RSA.pu0s-halox4tu7wmES1FVSx6u-4wc0YrUFXcqWXZG4-27UmbCOpMQftRCldNRfyA-qLbz-eqiwrong1EwUvjsD4cYbAHNGHwTvDOyx5AKthQUP44ykPv7kjKGh3DWKySJvcs9tlUG87hlo7AvnMo9pwRS_Zz2CacQ-MKaXyDepk=.AQAB" + }) - cng = - Ecto.Changeset.change(salmon_user) + salmon_user + |> Ecto.Changeset.change() |> Ecto.Changeset.put_embed(:info, info_cng) |> Repo.update() - conn = - build_conn() - |> put_req_header("content-type", "application/atom+xml") - |> post("/users/#{user.nickname}/salmon", salmon) + conn = + build_conn() + |> put_req_header("content-type", "application/atom+xml") + |> post("/users/#{user.nickname}/salmon", salmon) - assert response(conn, 200) + assert response(conn, 200) + end end test "gets a feed", %{conn: conn} do @@ -79,6 +92,7 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do conn = conn + |> put_req_header("accept", "application/xml") |> get(url) expected = @@ -92,82 +106,77 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do test "404s on private objects", %{conn: conn} do note_activity = insert(:direct_note_activity) - user = User.get_by_ap_id(note_activity.data["actor"]) [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["object"]["id"])) - url = "/objects/#{uuid}" - - conn = - conn - |> get(url) - assert response(conn, 404) + conn + |> get("/objects/#{uuid}") + |> response(404) end test "404s on nonexisting objects", %{conn: conn} do - url = "/objects/123" + conn + |> get("/objects/123") + |> response(404) + end - conn = - conn - |> get(url) + test "gets an activity in xml format", %{conn: conn} do + note_activity = insert(:note_activity) + [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"])) - assert response(conn, 404) + conn + |> put_req_header("accept", "application/xml") + |> get("/activities/#{uuid}") + |> response(200) end - test "gets an activity", %{conn: conn} do + test "404s on deleted objects", %{conn: conn} do note_activity = insert(:note_activity) - [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"])) - url = "/activities/#{uuid}" + [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["object"]["id"])) + object = Object.get_by_ap_id(note_activity.data["object"]["id"]) - conn = - conn - |> get(url) + conn + |> put_req_header("accept", "application/xml") + |> get("/objects/#{uuid}") + |> response(200) + + Object.delete(object) - assert response(conn, 200) + conn + |> put_req_header("accept", "application/xml") + |> get("/objects/#{uuid}") + |> response(404) end test "404s on private activities", %{conn: conn} do note_activity = insert(:direct_note_activity) [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"])) - url = "/activities/#{uuid}" - conn = - conn - |> get(url) - - assert response(conn, 404) + conn + |> get("/activities/#{uuid}") + |> response(404) end test "404s on nonexistent activities", %{conn: conn} do - url = "/activities/123" - - conn = - conn - |> get(url) - - assert response(conn, 404) + conn + |> get("/activities/123") + |> response(404) end - test "gets a notice", %{conn: conn} do + test "gets a notice in xml format", %{conn: conn} do note_activity = insert(:note_activity) - url = "/notice/#{note_activity.id}" - - conn = - conn - |> get(url) - assert response(conn, 200) + conn + |> get("/notice/#{note_activity.id}") + |> response(200) end test "gets a notice in AS2 format", %{conn: conn} do note_activity = insert(:note_activity) - url = "/notice/#{note_activity.id}" - conn = - conn - |> put_req_header("accept", "application/activity+json") - |> get(url) - - assert json_response(conn, 200) + conn + |> put_req_header("accept", "application/activity+json") + |> get("/notice/#{note_activity.id}") + |> json_response(200) end test "only gets a notice in AS2 format for Create messages", %{conn: conn} do diff --git a/test/web/ostatus/ostatus_test.exs b/test/web/ostatus/ostatus_test.exs index b5805c668..50467c71f 100644 --- a/test/web/ostatus/ostatus_test.exs +++ b/test/web/ostatus/ostatus_test.exs @@ -1,11 +1,24 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.OStatusTest do use Pleroma.DataCase + alias Pleroma.Activity + alias Pleroma.Instances + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.User alias Pleroma.Web.OStatus alias Pleroma.Web.XML - alias Pleroma.{Object, Repo, User, Activity} import Pleroma.Factory import ExUnit.CaptureLog + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + test "don't insert create notes twice" do incoming = File.read!("test/fixtures/incoming_note_activity.xml") {:ok, [activity]} = OStatus.handle_incoming(incoming) @@ -150,9 +163,8 @@ defmodule Pleroma.Web.OStatusTest do assert "https://pleroma.soykaf.com/users/lain" in activity.data["to"] refute activity.local - retweeted_activity = Repo.get(Activity, retweeted_activity.id) + retweeted_activity = Activity.get_by_id(retweeted_activity.id) retweeted_object = Object.normalize(retweeted_activity.data["object"]) - assert retweeted_activity.data["type"] == "Create" assert retweeted_activity.data["actor"] == "https://pleroma.soykaf.com/users/lain" refute retweeted_activity.local @@ -179,7 +191,7 @@ defmodule Pleroma.Web.OStatusTest do assert user.ap_id in activity.data["to"] refute activity.local - retweeted_activity = Repo.get(Activity, retweeted_activity.id) + retweeted_activity = Activity.get_by_id(retweeted_activity.id) assert note_activity.id == retweeted_activity.id assert retweeted_activity.data["type"] == "Create" assert retweeted_activity.data["actor"] == user.ap_id @@ -314,6 +326,22 @@ defmodule Pleroma.Web.OStatusTest do refute User.following?(follower, followed) end + test "it clears `unreachable` federation status of the sender" do + incoming_reaction_xml = File.read!("test/fixtures/share-gs.xml") + doc = XML.parse_document(incoming_reaction_xml) + actor_uri = XML.string_from_xpath("//author/uri[1]", doc) + reacted_to_author_uri = XML.string_from_xpath("//author/uri[2]", doc) + + Instances.set_consistently_unreachable(actor_uri) + Instances.set_consistently_unreachable(reacted_to_author_uri) + refute Instances.reachable?(actor_uri) + refute Instances.reachable?(reacted_to_author_uri) + + {:ok, _} = OStatus.handle_incoming(incoming_reaction_xml) + assert Instances.reachable?(actor_uri) + refute Instances.reachable?(reacted_to_author_uri) + end + describe "new remote user creation" do test "returns local users" do local_user = insert(:user) @@ -327,7 +355,7 @@ defmodule Pleroma.Web.OStatusTest do {:ok, user} = OStatus.find_or_make_user(uri) - user = Repo.get(Pleroma.User, user.id) + user = Pleroma.User.get_by_id(user.id) assert user.name == "Constance Variable" assert user.nickname == "lambadalambda@social.heldscal.la" assert user.local == false @@ -517,8 +545,10 @@ defmodule Pleroma.Web.OStatusTest do note_object.data |> Map.put("type", "Article") + Cachex.clear(:object_cache) + cs = Object.change(note_object, %{data: note_data}) - {:ok, article_object} = Repo.update(cs) + {:ok, _article_object} = Repo.update(cs) # the underlying object is now an Article instead of a note, so this should fail refute OStatus.is_representable?(note_activity) diff --git a/test/web/ostatus/user_representer_test.exs b/test/web/ostatus/user_representer_test.exs index 82fb8e793..e3863d2e9 100644 --- a/test/web/ostatus/user_representer_test.exs +++ b/test/web/ostatus/user_representer_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.OStatus.UserRepresenterTest do use Pleroma.DataCase alias Pleroma.Web.OStatus.UserRepresenter diff --git a/test/web/plugs/federating_plug_test.exs b/test/web/plugs/federating_plug_test.exs index 1455a1c46..612db7e32 100644 --- a/test/web/plugs/federating_plug_test.exs +++ b/test/web/plugs/federating_plug_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.FederatingPlugTest do use Pleroma.Web.ConnCase diff --git a/test/web/push/impl_test.exs b/test/web/push/impl_test.exs new file mode 100644 index 000000000..6bac2c9f6 --- /dev/null +++ b/test/web/push/impl_test.exs @@ -0,0 +1,147 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Push.ImplTest do + use Pleroma.DataCase + + alias Pleroma.Web.Push.Impl + alias Pleroma.Web.Push.Subscription + + import Pleroma.Factory + + setup_all do + Tesla.Mock.mock_global(fn + %{method: :post, url: "https://example.com/example/1234"} -> + %Tesla.Env{status: 200} + + %{method: :post, url: "https://example.com/example/not_found"} -> + %Tesla.Env{status: 400} + + %{method: :post, url: "https://example.com/example/bad"} -> + %Tesla.Env{status: 100} + end) + + :ok + end + + @sub %{ + endpoint: "https://example.com/example/1234", + keys: %{ + auth: "8eDyX_uCN0XRhSbY5hs7Hg==", + p256dh: + "BCIWgsnyXDv1VkhqL2P7YRBvdeuDnlwAPT2guNhdIoW3IP7GmHh1SMKPLxRf7x8vJy6ZFK3ol2ohgn_-0yP7QQA=" + } + } + @api_key "BASgACIHpN1GYgzSRp" + @message "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..." + + test "performs sending notifications" do + user = insert(:user) + user2 = insert(:user) + insert(:push_subscription, user: user, data: %{alerts: %{"mention" => true}}) + insert(:push_subscription, user: user2, data: %{alerts: %{"mention" => true}}) + + insert(:push_subscription, + user: user, + data: %{alerts: %{"follow" => true, "mention" => true}} + ) + + insert(:push_subscription, + user: user, + data: %{alerts: %{"follow" => true, "mention" => false}} + ) + + notif = + insert(:notification, + user: user, + activity: %Pleroma.Activity{ + data: %{ + "type" => "Create", + "actor" => user.ap_id, + "object" => %{"content" => "<Lorem ipsum dolor sit amet."} + } + } + ) + + assert Impl.perform(notif) == [:ok, :ok] + end + + @tag capture_log: true + test "returns error if notif does not match " do + assert Impl.perform(%{}) == :error + end + + test "successful message sending" do + assert Impl.push_message(@message, @sub, @api_key, %Subscription{}) == :ok + end + + @tag capture_log: true + test "fail message sending" do + assert Impl.push_message( + @message, + Map.merge(@sub, %{endpoint: "https://example.com/example/bad"}), + @api_key, + %Subscription{} + ) == :error + end + + test "delete subsciption if restult send message between 400..500" do + subscription = insert(:push_subscription) + + assert Impl.push_message( + @message, + Map.merge(@sub, %{endpoint: "https://example.com/example/not_found"}), + @api_key, + subscription + ) == :ok + + refute Pleroma.Repo.get(Subscription, subscription.id) + end + + test "renders body for create activity" do + assert Impl.format_body( + %{ + activity: %{ + data: %{ + "type" => "Create", + "object" => %{ + "content" => + "<span>Lorem ipsum dolor sit amet</span>, consectetur :bear: adipiscing elit. Fusce sagittis finibus turpis." + } + } + } + }, + %{nickname: "Bob"} + ) == + "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..." + end + + test "renders body for follow activity" do + assert Impl.format_body(%{activity: %{data: %{"type" => "Follow"}}}, %{nickname: "Bob"}) == + "@Bob has followed you" + end + + test "renders body for announce activity" do + user = insert(:user) + + note = + insert(:note, %{ + data: %{ + "content" => + "<span>Lorem ipsum dolor sit amet</span>, consectetur :bear: adipiscing elit. Fusce sagittis finibus turpis." + } + }) + + note_activity = insert(:note_activity, %{note: note}) + announce_activity = insert(:announce_activity, %{user: user, note_activity: note_activity}) + + assert Impl.format_body(%{activity: announce_activity}, user) == + "@#{user.nickname} repeated: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..." + end + + test "renders body for like activity" do + assert Impl.format_body(%{activity: %{data: %{"type" => "Like"}}}, %{nickname: "Bob"}) == + "@Bob has favorited your post" + end +end diff --git a/test/web/rel_me_test.exs b/test/web/rel_me_test.exs new file mode 100644 index 000000000..5188f4de1 --- /dev/null +++ b/test/web/rel_me_test.exs @@ -0,0 +1,67 @@ +defmodule Pleroma.Web.RelMeTest do + use ExUnit.Case, async: true + + setup do + Tesla.Mock.mock(fn + %{ + method: :get, + url: "http://example.com/rel_me/anchor" + } -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_anchor.html")} + + %{ + method: :get, + url: "http://example.com/rel_me/anchor_nofollow" + } -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_anchor_nofollow.html")} + + %{ + method: :get, + url: "http://example.com/rel_me/link" + } -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_link.html")} + + %{ + method: :get, + url: "http://example.com/rel_me/null" + } -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_null.html")} + end) + + :ok + end + + test "parse/1" do + hrefs = ["https://social.example.org/users/lain"] + + assert Pleroma.Web.RelMe.parse("http://example.com/rel_me/null") == {:ok, []} + assert {:error, _} = Pleroma.Web.RelMe.parse("http://example.com/rel_me/error") + + assert Pleroma.Web.RelMe.parse("http://example.com/rel_me/link") == {:ok, hrefs} + assert Pleroma.Web.RelMe.parse("http://example.com/rel_me/anchor") == {:ok, hrefs} + assert Pleroma.Web.RelMe.parse("http://example.com/rel_me/anchor_nofollow") == {:ok, hrefs} + end + + test "maybe_put_rel_me/2" do + profile_urls = ["https://social.example.org/users/lain"] + attr = "me" + fallback = nil + + assert Pleroma.Web.RelMe.maybe_put_rel_me("http://example.com/rel_me/null", profile_urls) == + fallback + + assert Pleroma.Web.RelMe.maybe_put_rel_me("http://example.com/rel_me/error", profile_urls) == + fallback + + assert Pleroma.Web.RelMe.maybe_put_rel_me("http://example.com/rel_me/anchor", profile_urls) == + attr + + assert Pleroma.Web.RelMe.maybe_put_rel_me( + "http://example.com/rel_me/anchor_nofollow", + profile_urls + ) == attr + + assert Pleroma.Web.RelMe.maybe_put_rel_me("http://example.com/rel_me/link", profile_urls) == + attr + end +end diff --git a/test/web/retry_queue_test.exs b/test/web/retry_queue_test.exs index ce2964993..ecb3ce5d0 100644 --- a/test/web/retry_queue_test.exs +++ b/test/web/retry_queue_test.exs @@ -1,31 +1,48 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule MockActivityPub do - def publish_one(ret) do + def publish_one({ret, waiter}) do + send(waiter, :complete) {ret, "success"} end end -defmodule Pleroma.ActivityTest do +defmodule Pleroma.Web.Federator.RetryQueueTest do use Pleroma.DataCase alias Pleroma.Web.Federator.RetryQueue @small_retry_count 0 @hopeless_retry_count 10 + setup do + RetryQueue.reset_stats() + end + + test "RetryQueue responds to stats request" do + assert %{delivered: 0, dropped: 0} == RetryQueue.get_stats() + end + test "failed posts are retried" do {:retry, _timeout} = RetryQueue.get_retry_params(@small_retry_count) - assert {:noreply, %{delivered: 1}} == - RetryQueue.handle_info({:send, :ok, MockActivityPub, @small_retry_count}, %{ - delivered: 0 - }) + wait_task = + Task.async(fn -> + receive do + :complete -> :ok + end + end) + + RetryQueue.enqueue({:ok, wait_task.pid}, MockActivityPub, @small_retry_count) + Task.await(wait_task) + assert %{delivered: 1, dropped: 0} == RetryQueue.get_stats() end test "posts that have been tried too many times are dropped" do {:drop, _timeout} = RetryQueue.get_retry_params(@hopeless_retry_count) - assert {:noreply, %{dropped: 1}} == - RetryQueue.handle_cast({:maybe_enqueue, %{}, nil, @hopeless_retry_count}, %{ - dropped: 0 - }) + RetryQueue.enqueue({:ok, nil}, MockActivityPub, @hopeless_retry_count) + assert %{delivered: 0, dropped: 1} == RetryQueue.get_stats() end end diff --git a/test/web/rich_media/helpers_test.exs b/test/web/rich_media/helpers_test.exs new file mode 100644 index 000000000..60d93768f --- /dev/null +++ b/test/web/rich_media/helpers_test.exs @@ -0,0 +1,62 @@ +defmodule Pleroma.Web.RichMedia.HelpersTest do + use Pleroma.DataCase + + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + import Tesla.Mock + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + test "refuses to crawl incomplete URLs" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "[test](example.com/ogp)", + "content_type" => "text/markdown" + }) + + Pleroma.Config.put([:rich_media, :enabled], true) + + assert %{} == Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) + + Pleroma.Config.put([:rich_media, :enabled], false) + end + + test "refuses to crawl malformed URLs" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "[test](example.com[]/ogp)", + "content_type" => "text/markdown" + }) + + Pleroma.Config.put([:rich_media, :enabled], true) + + assert %{} == Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) + + Pleroma.Config.put([:rich_media, :enabled], false) + end + + test "crawls valid, complete URLs" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "[test](http://example.com/ogp)", + "content_type" => "text/markdown" + }) + + Pleroma.Config.put([:rich_media, :enabled], true) + + assert %{page_url: "http://example.com/ogp", rich_media: _} = + Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) + + Pleroma.Config.put([:rich_media, :enabled], false) + end +end diff --git a/test/web/rich_media/parser_test.exs b/test/web/rich_media/parser_test.exs new file mode 100644 index 000000000..47b127cf9 --- /dev/null +++ b/test/web/rich_media/parser_test.exs @@ -0,0 +1,95 @@ +defmodule Pleroma.Web.RichMedia.ParserTest do + use ExUnit.Case, async: true + + setup do + Tesla.Mock.mock(fn + %{ + method: :get, + url: "http://example.com/ogp" + } -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/ogp.html")} + + %{ + method: :get, + url: "http://example.com/twitter-card" + } -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/twitter_card.html")} + + %{ + method: :get, + url: "http://example.com/oembed" + } -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/oembed.html")} + + %{ + method: :get, + url: "http://example.com/oembed.json" + } -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/oembed.json")} + + %{method: :get, url: "http://example.com/empty"} -> + %Tesla.Env{status: 200, body: "hello"} + end) + + :ok + end + + test "returns error when no metadata present" do + assert {:error, _} = Pleroma.Web.RichMedia.Parser.parse("http://example.com/empty") + end + + test "parses ogp" do + assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/ogp") == + {:ok, + %{ + image: "http://ia.media-imdb.com/images/rock.jpg", + title: "The Rock", + type: "video.movie", + url: "http://www.imdb.com/title/tt0117500/" + }} + end + + test "parses twitter card" do + assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/twitter-card") == + {:ok, + %{ + card: "summary", + site: "@flickr", + image: "https://farm6.staticflickr.com/5510/14338202952_93595258ff_z.jpg", + title: "Small Island Developing States Photo Submission", + description: "View the album on Flickr." + }} + end + + test "parses OEmbed" do + assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/oembed") == + {:ok, + %{ + author_name: "bees", + author_url: "https://www.flickr.com/photos/bees/", + cache_age: 3600, + flickr_type: "photo", + height: "768", + html: + "<a data-flickr-embed=\"true\" href=\"https://www.flickr.com/photos/bees/2362225867/\" title=\"Bacon Lollys by bees, on Flickr\"><img src=\"https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_b.jpg\" width=\"1024\" height=\"768\" alt=\"Bacon Lollys\"></a><script async src=\"https://embedr.flickr.com/assets/client-code.js\" charset=\"utf-8\"></script>", + license: "All Rights Reserved", + license_id: 0, + provider_name: "Flickr", + provider_url: "https://www.flickr.com/", + thumbnail_height: 150, + thumbnail_url: "https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_q.jpg", + thumbnail_width: 150, + title: "Bacon Lollys", + type: "photo", + url: "https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_b.jpg", + version: "1.0", + web_page: "https://www.flickr.com/photos/bees/2362225867/", + web_page_short_url: "https://flic.kr/p/4AK2sc", + width: "1024" + }} + end + + test "rejects invalid OGP data" do + assert {:error, _} = Pleroma.Web.RichMedia.Parser.parse("http://example.com/malformed") + end +end diff --git a/test/web/salmon/salmon_test.exs b/test/web/salmon/salmon_test.exs index 1b39b4b2d..35503259b 100644 --- a/test/web/salmon/salmon_test.exs +++ b/test/web/salmon/salmon_test.exs @@ -1,7 +1,13 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.Salmon.SalmonTest do use Pleroma.DataCase + alias Pleroma.Activity + alias Pleroma.Repo + alias Pleroma.User alias Pleroma.Web.Salmon - alias Pleroma.{Repo, Activity, User} import Pleroma.Factory @magickey "RSA.pu0s-halox4tu7wmES1FVSx6u-4wc0YrUFXcqWXZG4-27UmbCOpMQftRCldNRfyA-qLbz-eqiwQhh-1EwUvjsD4cYbAHNGHwTvDOyx5AKthQUP44ykPv7kjKGh3DWKySJvcs9tlUG87hlo7AvnMo9pwRS_Zz2CacQ-MKaXyDepk=.AQAB" @@ -10,6 +16,11 @@ defmodule Pleroma.Web.Salmon.SalmonTest do @magickey_friendica "RSA.AMwa8FUs2fWEjX0xN7yRQgegQffhBpuKNC6fa5VNSVorFjGZhRrlPMn7TQOeihlc9lBz2OsHlIedbYn2uJ7yCs0.AQAB" + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + test "decodes a salmon" do {:ok, salmon} = File.read("test/fixtures/salmon.xml") {:ok, doc} = Salmon.decode_and_validate(@magickey, salmon) @@ -69,7 +80,7 @@ defmodule Pleroma.Web.Salmon.SalmonTest do test "it pushes an activity to remote accounts it's addressed to" do user_data = %{ info: %{ - "salmon" => "http://example.org/salmon" + salmon: "http://test-example.org/salmon" }, local: false } @@ -88,11 +99,11 @@ defmodule Pleroma.Web.Salmon.SalmonTest do } {:ok, activity} = Repo.insert(%Activity{data: activity_data, recipients: activity_data["to"]}) - user = Repo.get_by(User, ap_id: activity.data["actor"]) + user = User.get_by_ap_id(activity.data["actor"]) {:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user) - poster = fn url, _data, _headers, _options -> - assert url == "http://example.org/salmon" + poster = fn url, _data, _headers -> + assert url == "http://test-example.org/salmon" end Salmon.publish(user, activity, poster) diff --git a/test/web/streamer_test.exs b/test/web/streamer_test.exs index df58441f0..bfe18cb7f 100644 --- a/test/web/streamer_test.exs +++ b/test/web/streamer_test.exs @@ -1,9 +1,14 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.StreamerTest do use Pleroma.DataCase - alias Pleroma.Web.Streamer - alias Pleroma.{List, User} + alias Pleroma.List + alias Pleroma.User alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Streamer import Pleroma.Factory test "it sends to public" do @@ -31,6 +36,36 @@ defmodule Pleroma.Web.StreamerTest do Streamer.push_to_socket(topics, "public", activity) Task.await(task) + + task = + Task.async(fn -> + expected_event = + %{ + "event" => "delete", + "payload" => activity.id + } + |> Jason.encode!() + + assert_receive {:text, received_event}, 4_000 + assert received_event == expected_event + end) + + fake_socket = %{ + transport_pid: task.pid, + assigns: %{ + user: user + } + } + + {:ok, activity} = CommonAPI.delete(activity.id, other_user) + + topics = %{ + "public" => [fake_socket] + } + + Streamer.push_to_socket(topics, "public", activity) + + Task.await(task) end test "it doesn't send to blocked users" do @@ -167,4 +202,34 @@ defmodule Pleroma.Web.StreamerTest do Task.await(task) end + + test "it doesn't send muted reblogs" do + user1 = insert(:user) + user2 = insert(:user) + user3 = insert(:user) + CommonAPI.hide_reblogs(user1, user2) + + task = + Task.async(fn -> + refute_receive {:text, _}, 1_000 + end) + + fake_socket = %{ + transport_pid: task.pid, + assigns: %{ + user: user1 + } + } + + {:ok, create_activity} = CommonAPI.post(user3, %{"status" => "I'm kawen"}) + {:ok, announce_activity, _} = CommonAPI.repeat(create_activity.id, user2) + + topics = %{ + "public" => [fake_socket] + } + + Streamer.push_to_socket(topics, "public", announce_activity) + + Task.await(task) + end end diff --git a/test/web/twitter_api/representers/activity_representer_test.exs b/test/web/twitter_api/representers/activity_representer_test.exs deleted file mode 100644 index 314f2b51f..000000000 --- a/test/web/twitter_api/representers/activity_representer_test.exs +++ /dev/null @@ -1,200 +0,0 @@ -defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenterTest do - use Pleroma.DataCase - alias Pleroma.{User, Activity, Object} - alias Pleroma.Web.TwitterAPI.Representers.{ActivityRepresenter, ObjectRepresenter} - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Builders.UserBuilder - alias Pleroma.Web.TwitterAPI.UserView - import Pleroma.Factory - - test "an announce activity" do - user = insert(:user) - note_activity = insert(:note_activity) - activity_actor = Repo.get_by(User, ap_id: note_activity.data["actor"]) - object = Object.get_by_ap_id(note_activity.data["object"]["id"]) - - {:ok, announce_activity, _object} = ActivityPub.announce(user, object) - note_activity = Activity.get_by_ap_id(note_activity.data["id"]) - - status = - ActivityRepresenter.to_map(announce_activity, %{ - users: [user, activity_actor], - announced_activity: note_activity, - for: user - }) - - assert status["id"] == announce_activity.id - assert status["user"] == UserView.render("show.json", %{user: user, for: user}) - - retweeted_status = - ActivityRepresenter.to_map(note_activity, %{user: activity_actor, for: user}) - - assert retweeted_status["repeated"] == true - assert retweeted_status["id"] == note_activity.id - assert status["statusnet_conversation_id"] == retweeted_status["statusnet_conversation_id"] - - assert status["retweeted_status"] == retweeted_status - assert status["activity_type"] == "repeat" - end - - test "a like activity" do - user = insert(:user) - note_activity = insert(:note_activity) - object = Object.get_by_ap_id(note_activity.data["object"]["id"]) - - {:ok, like_activity, _object} = ActivityPub.like(user, object) - - status = - ActivityRepresenter.to_map(like_activity, %{user: user, liked_activity: note_activity}) - - assert status["id"] == like_activity.id - assert status["in_reply_to_status_id"] == note_activity.id - - note_activity = Activity.get_by_ap_id(note_activity.data["id"]) - activity_actor = Repo.get_by(User, ap_id: note_activity.data["actor"]) - liked_status = ActivityRepresenter.to_map(note_activity, %{user: activity_actor, for: user}) - assert liked_status["favorited"] == true - assert status["activity_type"] == "like" - end - - test "an activity" do - user = insert(:user) - # {:ok, mentioned_user } = UserBuilder.insert(%{nickname: "shp", ap_id: "shp"}) - mentioned_user = insert(:user, %{nickname: "shp"}) - - # {:ok, follower} = UserBuilder.insert(%{following: [User.ap_followers(user)]}) - follower = insert(:user, %{following: [User.ap_followers(user)]}) - - object = %Object{ - data: %{ - "type" => "Image", - "url" => [ - %{ - "type" => "Link", - "mediaType" => "image/jpg", - "href" => "http://example.org/image.jpg" - } - ], - "uuid" => 1 - } - } - - content_html = - "<script>alert('YAY')</script>Some :2hu: content mentioning <a href='#{mentioned_user.ap_id}'>@shp</shp>" - - content = HtmlSanitizeEx.strip_tags(content_html) - date = DateTime.from_naive!(~N[2016-05-24 13:26:08.003], "Etc/UTC") |> DateTime.to_iso8601() - - {:ok, convo_object} = Object.context_mapping("2hu") |> Repo.insert() - - note_object = %{ - "id" => "https://example.com/id/1", - "published" => date, - "type" => "Note", - "content" => content_html, - "summary" => "2hu", - "inReplyToStatusId" => 213_123, - "attachment" => [object.data], - "external_url" => "some url", - "like_count" => 5, - "announcement_count" => 3, - "context" => "2hu", - "tag" => ["content", "mentioning", "nsfw"], - "emoji" => %{ - "2hu" => "corndog.png" - } - } - - Object.create(note_object) - - to = [ - User.ap_followers(user), - "https://www.w3.org/ns/activitystreams#Public", - mentioned_user.ap_id - ] - - activity = %Activity{ - id: 1, - data: %{ - "type" => "Create", - "id" => "id", - "to" => to, - "actor" => User.ap_id(user), - "object" => note_object["id"], - "published" => date, - "context" => "2hu" - }, - local: false, - recipients: to - } - - expected_html = - "<p>2hu</p>alert('YAY')Some <img height=\"32px\" width=\"32px\" alt=\"2hu\" title=\"2hu\" src=\"corndog.png\" /> content mentioning <a href=\"#{ - mentioned_user.ap_id - }\">@shp</a>" - - expected_status = %{ - "id" => activity.id, - "user" => UserView.render("show.json", %{user: user, for: follower}), - "is_local" => false, - "statusnet_html" => expected_html, - "text" => "2hu" <> content, - "is_post_verb" => true, - "created_at" => "Tue May 24 13:26:08 +0000 2016", - "in_reply_to_status_id" => 213_123, - "in_reply_to_screen_name" => nil, - "in_reply_to_user_id" => nil, - "in_reply_to_profileurl" => nil, - "in_reply_to_ostatus_uri" => nil, - "statusnet_conversation_id" => convo_object.id, - "attachments" => [ - ObjectRepresenter.to_map(object) - ], - "attentions" => [ - UserView.render("show.json", %{user: mentioned_user, for: follower}) - ], - "fave_num" => 5, - "repeat_num" => 3, - "favorited" => false, - "repeated" => false, - "external_url" => "some url", - "tags" => ["nsfw", "content", "mentioning"], - "activity_type" => "post", - "possibly_sensitive" => true, - "uri" => note_object["id"], - "visibility" => "direct", - "summary" => "2hu" - } - - assert ActivityRepresenter.to_map(activity, %{ - user: user, - for: follower, - mentioned: [mentioned_user] - }) == expected_status - end - - test "an undo for a follow" do - follower = insert(:user) - followed = insert(:user) - - {:ok, _follow} = ActivityPub.follow(follower, followed) - {:ok, unfollow} = ActivityPub.unfollow(follower, followed) - - map = ActivityRepresenter.to_map(unfollow, %{user: follower}) - assert map["is_post_verb"] == false - assert map["activity_type"] == "undo" - end - - test "a delete activity" do - object = insert(:note) - user = User.get_by_ap_id(object.data["actor"]) - - {:ok, delete} = ActivityPub.delete(object) - - map = ActivityRepresenter.to_map(delete, %{user: user}) - - assert map["is_post_verb"] == false - assert map["activity_type"] == "delete" - assert map["uri"] == object.data["id"] - end -end diff --git a/test/web/twitter_api/representers/object_representer_test.exs b/test/web/twitter_api/representers/object_representer_test.exs index 228b2ac42..c3cf330f1 100644 --- a/test/web/twitter_api/representers/object_representer_test.exs +++ b/test/web/twitter_api/representers/object_representer_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.TwitterAPI.Representers.ObjectReprenterTest do use Pleroma.DataCase diff --git a/test/web/twitter_api/twitter_api_controller_test.exs b/test/web/twitter_api/twitter_api_controller_test.exs index a6495ffc1..9a9630c19 100644 --- a/test/web/twitter_api/twitter_api_controller_test.exs +++ b/test/web/twitter_api/twitter_api_controller_test.exs @@ -1,31 +1,43 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.TwitterAPI.ControllerTest do use Pleroma.Web.ConnCase - alias Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter - alias Pleroma.Builders.{ActivityBuilder, UserBuilder} - alias Pleroma.{Repo, Activity, User, Object, Notification} + alias Comeonin.Pbkdf2 + alias Ecto.Changeset + alias Pleroma.Activity + alias Pleroma.Builders.ActivityBuilder + alias Pleroma.Builders.UserBuilder + alias Pleroma.Notification + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.TwitterAPI.UserView - alias Pleroma.Web.TwitterAPI.NotificationView alias Pleroma.Web.CommonAPI + alias Pleroma.Web.OAuth.Token + alias Pleroma.Web.TwitterAPI.ActivityView + alias Pleroma.Web.TwitterAPI.Controller + alias Pleroma.Web.TwitterAPI.NotificationView alias Pleroma.Web.TwitterAPI.TwitterAPI - alias Comeonin.Pbkdf2 + alias Pleroma.Web.TwitterAPI.UserView + import Mock import Pleroma.Factory + import Swoosh.TestAssertions + + @banner "data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7" describe "POST /api/account/update_profile_banner" do test "it updates the banner", %{conn: conn} do user = insert(:user) - new_banner = - "data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7" - - response = - conn - |> assign(:user, user) - |> post(authenticated_twitter_api__path(conn, :update_banner), %{"banner" => new_banner}) - |> json_response(200) + conn + |> assign(:user, user) + |> post(authenticated_twitter_api__path(conn, :update_banner), %{"banner" => @banner}) + |> json_response(200) - user = Repo.get(User, user.id) + user = refresh_record(user) assert user.info.banner["type"] == "Image" end end @@ -34,16 +46,12 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do test "it updates the background", %{conn: conn} do user = insert(:user) - new_bg = - "data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7" - - response = - conn - |> assign(:user, user) - |> post(authenticated_twitter_api__path(conn, :update_background), %{"img" => new_bg}) - |> json_response(200) + conn + |> assign(:user, user) + |> post(authenticated_twitter_api__path(conn, :update_background), %{"img" => @banner}) + |> json_response(200) - user = Repo.get(User, user.id) + user = refresh_record(user) assert user.info.background["type"] == "Image" end end @@ -57,13 +65,14 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do end test "with credentials", %{conn: conn, user: user} do - conn = + response = conn |> with_credentials(user.nickname, "test") |> post("/api/account/verify_credentials.json") + |> json_response(200) - assert response = json_response(conn, 200) - assert response == UserView.render("show.json", %{user: user, token: response["token"]}) + assert response == + UserView.render("show.json", %{user: user, token: response["token"], for: user}) end end @@ -84,24 +93,41 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do "error" => "Client must provide a 'status' parameter with a value." } - conn = conn_with_creds |> post(request_path) + conn = + conn_with_creds + |> post(request_path) + assert json_response(conn, 400) == error_response - conn = conn_with_creds |> post(request_path, %{status: ""}) + conn = + conn_with_creds + |> post(request_path, %{status: ""}) + assert json_response(conn, 400) == error_response - conn = conn_with_creds |> post(request_path, %{status: " "}) + conn = + conn_with_creds + |> post(request_path, %{status: " "}) + assert json_response(conn, 400) == error_response # we post with visibility private in order to avoid triggering relay - conn = conn_with_creds |> post(request_path, %{status: "Nice meme.", visibility: "private"}) + conn = + conn_with_creds + |> post(request_path, %{status: "Nice meme.", visibility: "private"}) assert json_response(conn, 200) == - ActivityRepresenter.to_map(Repo.one(Activity), %{user: user}) + ActivityView.render("activity.json", %{ + activity: Repo.one(Activity), + user: user, + for: user + }) end end describe "GET /statuses/public_timeline.json" do + setup [:valid_user] + test "returns statuses", %{conn: conn} do user = insert(:user) activities = ActivityBuilder.insert_list(30, %{}, %{user: user}) @@ -117,7 +143,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do assert length(response) == 10 end - test "returns 403 to unauthenticated request when the instance is not public" do + test "returns 403 to unauthenticated request when the instance is not public", %{conn: conn} do instance = Application.get_env(:pleroma, :instance) |> Keyword.put(:public, false) @@ -135,15 +161,59 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do Application.put_env(:pleroma, :instance, instance) end - test "returns 200 to unauthenticated request when the instance is public" do + test "returns 200 to authenticated request when the instance is not public", + %{conn: conn, user: user} do + instance = + Application.get_env(:pleroma, :instance) + |> Keyword.put(:public, false) + + Application.put_env(:pleroma, :instance, instance) + + conn + |> with_credentials(user.nickname, "test") + |> get("/api/statuses/public_timeline.json") + |> json_response(200) + + instance = + Application.get_env(:pleroma, :instance) + |> Keyword.put(:public, true) + + Application.put_env(:pleroma, :instance, instance) + end + + test "returns 200 to unauthenticated request when the instance is public", %{conn: conn} do conn |> get("/api/statuses/public_timeline.json") |> json_response(200) end + + test "returns 200 to authenticated request when the instance is public", + %{conn: conn, user: user} do + conn + |> with_credentials(user.nickname, "test") + |> get("/api/statuses/public_timeline.json") + |> json_response(200) + end + + test_with_mock "treats user as unauthenticated if `assigns[:token]` is present but lacks `read` permission", + Controller, + [:passthrough], + [] do + token = insert(:oauth_token, scopes: ["write"]) + + build_conn() + |> put_req_header("authorization", "Bearer #{token.token}") + |> get("/api/statuses/public_timeline.json") + |> json_response(200) + + assert called(Controller.public_timeline(%{assigns: %{user: nil}}, :_)) + end end describe "GET /statuses/public_and_external_timeline.json" do - test "returns 403 to unauthenticated request when the instance is not public" do + setup [:valid_user] + + test "returns 403 to unauthenticated request when the instance is not public", %{conn: conn} do instance = Application.get_env(:pleroma, :instance) |> Keyword.put(:public, false) @@ -161,8 +231,36 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do Application.put_env(:pleroma, :instance, instance) end - test "returns 200 to unauthenticated request when the instance is public" do + test "returns 200 to authenticated request when the instance is not public", + %{conn: conn, user: user} do + instance = + Application.get_env(:pleroma, :instance) + |> Keyword.put(:public, false) + + Application.put_env(:pleroma, :instance, instance) + + conn + |> with_credentials(user.nickname, "test") + |> get("/api/statuses/public_and_external_timeline.json") + |> json_response(200) + + instance = + Application.get_env(:pleroma, :instance) + |> Keyword.put(:public, true) + + Application.put_env(:pleroma, :instance, instance) + end + + test "returns 200 to unauthenticated request when the instance is public", %{conn: conn} do + conn + |> get("/api/statuses/public_and_external_timeline.json") + |> json_response(200) + end + + test "returns 200 to authenticated request when the instance is public", + %{conn: conn, user: user} do conn + |> with_credentials(user.nickname, "test") |> get("/api/statuses/public_and_external_timeline.json") |> json_response(200) end @@ -180,7 +278,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do response = json_response(conn, 200) - assert response == ActivityRepresenter.to_map(activity, %{user: actor}) + assert response == ActivityView.render("activity.json", %{activity: activity, user: actor}) end end @@ -265,7 +363,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do since_id = List.last(activities).id current_user = - Ecto.Changeset.change(current_user, following: [User.ap_followers(user)]) + Changeset.change(current_user, following: [User.ap_followers(user)]) |> Repo.update!() conn = @@ -279,7 +377,8 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do assert response == Enum.map(returned_activities, fn activity -> - ActivityRepresenter.to_map(activity, %{ + ActivityView.render("activity.json", %{ + activity: activity, user: User.get_cached_by_ap_id(activity.data["actor"]), for: current_user }) @@ -322,6 +421,33 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do assert status["id"] == direct_two.id assert status_two["id"] == direct.id end + + test "doesn't include DMs from blocked users", %{conn: conn} do + blocker = insert(:user) + blocked = insert(:user) + user = insert(:user) + {:ok, blocker} = User.block(blocker, blocked) + + {:ok, _blocked_direct} = + CommonAPI.post(blocked, %{ + "status" => "Hi @#{blocker.nickname}!", + "visibility" => "direct" + }) + + {:ok, direct} = + CommonAPI.post(user, %{ + "status" => "Hi @#{blocker.nickname}!", + "visibility" => "direct" + }) + + res_conn = + conn + |> assign(:user, blocker) + |> get("/api/statuses/dm_timeline.json") + + [status] = json_response(res_conn, 200) + assert status["id"] == direct.id + end end describe "GET /statuses/mentions.json" do @@ -334,7 +460,10 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do test "with credentials", %{conn: conn, user: current_user} do {:ok, activity} = - ActivityBuilder.insert(%{"to" => [current_user.ap_id]}, %{user: current_user}) + CommonAPI.post(current_user, %{ + "status" => "why is tenshi eating a corndog so cute?", + "visibility" => "public" + }) conn = conn @@ -346,11 +475,29 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do assert length(response) == 1 assert Enum.at(response, 0) == - ActivityRepresenter.to_map(activity, %{ + ActivityView.render("activity.json", %{ user: current_user, - mentioned: [current_user] + for: current_user, + activity: activity }) end + + test "does not show DMs in mentions timeline", %{conn: conn, user: current_user} do + {:ok, _activity} = + CommonAPI.post(current_user, %{ + "status" => "Have you guys ever seen how cute tenshi eating a corndog is?", + "visibility" => "direct" + }) + + conn = + conn + |> with_credentials(current_user.nickname, "test") + |> get("/api/statuses/mentions.json") + + response = json_response(conn, 200) + + assert Enum.empty?(response) + end end describe "GET /api/qvitter/statuses/notifications.json" do @@ -453,7 +600,9 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do conn = get(conn, "/api/statuses/user_timeline.json", %{"user_id" => user.id}) response = json_response(conn, 200) assert length(response) == 1 - assert Enum.at(response, 0) == ActivityRepresenter.to_map(activity, %{user: user}) + + assert Enum.at(response, 0) == + ActivityView.render("activity.json", %{user: user, activity: activity}) end test "with screen_name", %{conn: conn} do @@ -463,7 +612,9 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do conn = get(conn, "/api/statuses/user_timeline.json", %{"screen_name" => user.nickname}) response = json_response(conn, 200) assert length(response) == 1 - assert Enum.at(response, 0) == ActivityRepresenter.to_map(activity, %{user: user}) + + assert Enum.at(response, 0) == + ActivityView.render("activity.json", %{user: user, activity: activity}) end test "with credentials", %{conn: conn, user: current_user} do @@ -477,7 +628,13 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do response = json_response(conn, 200) assert length(response) == 1 - assert Enum.at(response, 0) == ActivityRepresenter.to_map(activity, %{user: current_user}) + + assert Enum.at(response, 0) == + ActivityView.render("activity.json", %{ + user: current_user, + for: current_user, + activity: activity + }) end test "with credentials with user_id", %{conn: conn, user: current_user} do @@ -492,7 +649,9 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do response = json_response(conn, 200) assert length(response) == 1 - assert Enum.at(response, 0) == ActivityRepresenter.to_map(activity, %{user: user}) + + assert Enum.at(response, 0) == + ActivityView.render("activity.json", %{user: user, activity: activity}) end test "with credentials screen_name", %{conn: conn, user: current_user} do @@ -507,7 +666,41 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do response = json_response(conn, 200) assert length(response) == 1 - assert Enum.at(response, 0) == ActivityRepresenter.to_map(activity, %{user: user}) + + assert Enum.at(response, 0) == + ActivityView.render("activity.json", %{user: user, activity: activity}) + end + + test "with credentials with user_id, excluding RTs", %{conn: conn, user: current_user} do + user = insert(:user) + {:ok, activity} = ActivityBuilder.insert(%{"id" => 1, "type" => "Create"}, %{user: user}) + {:ok, _} = ActivityBuilder.insert(%{"id" => 2, "type" => "Announce"}, %{user: user}) + + conn = + conn + |> with_credentials(current_user.nickname, "test") + |> get("/api/statuses/user_timeline.json", %{ + "user_id" => user.id, + "include_rts" => "false" + }) + + response = json_response(conn, 200) + + assert length(response) == 1 + + assert Enum.at(response, 0) == + ActivityView.render("activity.json", %{user: user, activity: activity}) + + conn = + conn + |> get("/api/statuses/user_timeline.json", %{"user_id" => user.id, "include_rts" => "0"}) + + response = json_response(conn, 200) + + assert length(response) == 1 + + assert Enum.at(response, 0) == + ActivityView.render("activity.json", %{user: user, activity: activity}) end end @@ -527,12 +720,29 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do |> with_credentials(current_user.nickname, "test") |> post("/api/friendships/create.json", %{user_id: followed.id}) - current_user = Repo.get(User, current_user.id) + current_user = User.get_by_id(current_user.id) assert User.ap_followers(followed) in current_user.following assert json_response(conn, 200) == UserView.render("show.json", %{user: followed, for: current_user}) end + + test "for restricted account", %{conn: conn, user: current_user} do + followed = insert(:user, info: %User.Info{locked: true}) + + conn = + conn + |> with_credentials(current_user.nickname, "test") + |> post("/api/friendships/create.json", %{user_id: followed.id}) + + current_user = User.get_by_id(current_user.id) + followed = User.get_by_id(followed.id) + + refute User.ap_followers(followed) in current_user.following + + assert json_response(conn, 200) == + UserView.render("show.json", %{user: followed, for: current_user}) + end end describe "POST /friendships/destroy.json" do @@ -555,7 +765,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do |> with_credentials(current_user.nickname, "test") |> post("/api/friendships/destroy.json", %{user_id: followed.id}) - current_user = Repo.get(User, current_user.id) + current_user = User.get_by_id(current_user.id) assert current_user.following == [current_user.ap_id] assert json_response(conn, 200) == @@ -579,7 +789,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do |> with_credentials(current_user.nickname, "test") |> post("/api/blocks/create.json", %{user_id: blocked.id}) - current_user = Repo.get(User, current_user.id) + current_user = User.get_by_id(current_user.id) assert User.blocks?(current_user, blocked) assert json_response(conn, 200) == @@ -606,7 +816,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do |> with_credentials(current_user.nickname, "test") |> post("/api/blocks/destroy.json", %{user_id: blocked.id}) - current_user = Repo.get(User, current_user.id) + current_user = User.get_by_id(current_user.id) assert current_user.info.blocks == [] assert json_response(conn, 200) == @@ -637,7 +847,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do |> with_credentials(current_user.nickname, "test") |> post("/api/qvitter/update_avatar.json", %{img: avatar_image}) - current_user = Repo.get(User, current_user.id) + current_user = User.get_by_id(current_user.id) assert is_map(current_user.avatar) assert json_response(conn, 200) == @@ -654,14 +864,13 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do end test "unimplemented mutes with credentials", %{conn: conn, user: current_user} do - conn = + response = conn |> with_credentials(current_user.nickname, "test") |> get("/api/qvitter/mutes.json") + |> json_response(200) - current_user = Repo.get(User, current_user.id) - - assert [] = json_response(conn, 200) + assert [] = response end end @@ -700,7 +909,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do |> with_credentials(current_user.nickname, "test") |> post("/api/favorites/create/1.json") - assert json_response(conn, 500) + assert json_response(conn, 400) end end @@ -746,11 +955,15 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do |> with_credentials(current_user.nickname, "test") |> post(request_path) - activity = Repo.get(Activity, note_activity.id) - activity_user = Repo.get_by(User, ap_id: note_activity.data["actor"]) + activity = Activity.get_by_id(note_activity.id) + activity_user = User.get_by_ap_id(note_activity.data["actor"]) assert json_response(response, 200) == - ActivityRepresenter.to_map(activity, %{user: activity_user, for: current_user}) + ActivityView.render("activity.json", %{ + user: activity_user, + for: current_user, + activity: activity + }) end end @@ -780,11 +993,15 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do |> with_credentials(current_user.nickname, "test") |> post(request_path) - activity = Repo.get(Activity, note_activity.id) - activity_user = Repo.get_by(User, ap_id: note_activity.data["actor"]) + activity = Activity.get_by_id(note_activity.id) + activity_user = User.get_by_ap_id(note_activity.data["actor"]) assert json_response(response, 200) == - ActivityRepresenter.to_map(activity, %{user: activity_user, for: current_user}) + ActivityView.render("activity.json", %{ + user: activity_user, + for: current_user, + activity: activity + }) end end @@ -805,7 +1022,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do user = json_response(conn, 200) - fetched_user = Repo.get_by(User, nickname: "lain") + fetched_user = User.get_by_nickname("lain") assert user == UserView.render("show.json", %{user: fetched_user}) end @@ -828,6 +1045,143 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do end end + describe "POST /api/account/password_reset, with valid parameters" do + setup %{conn: conn} do + user = insert(:user) + conn = post(conn, "/api/account/password_reset?email=#{user.email}") + %{conn: conn, user: user} + end + + test "it returns 204", %{conn: conn} do + assert json_response(conn, :no_content) + end + + test "it creates a PasswordResetToken record for user", %{user: user} do + token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id) + assert token_record + end + + test "it sends an email to user", %{user: user} do + token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id) + + email = Pleroma.Emails.UserEmail.password_reset_email(user, token_record.token) + notify_email = Pleroma.Config.get([:instance, :notify_email]) + instance_name = Pleroma.Config.get([:instance, :name]) + + assert_email_sent( + from: {instance_name, notify_email}, + to: {user.name, user.email}, + html_body: email.html_body + ) + end + end + + describe "POST /api/account/password_reset, with invalid parameters" do + setup [:valid_user] + + test "it returns 500 when user is not found", %{conn: conn, user: user} do + conn = post(conn, "/api/account/password_reset?email=nonexisting_#{user.email}") + assert json_response(conn, :internal_server_error) + end + + test "it returns 500 when user is not local", %{conn: conn, user: user} do + {:ok, user} = Repo.update(Changeset.change(user, local: false)) + conn = post(conn, "/api/account/password_reset?email=#{user.email}") + assert json_response(conn, :internal_server_error) + end + end + + describe "GET /api/account/confirm_email/:id/:token" do + setup do + user = insert(:user) + info_change = User.Info.confirmation_changeset(user.info, :unconfirmed) + + {:ok, user} = + user + |> Changeset.change() + |> Changeset.put_embed(:info, info_change) + |> Repo.update() + + assert user.info.confirmation_pending + + [user: user] + end + + test "it redirects to root url", %{conn: conn, user: user} do + conn = get(conn, "/api/account/confirm_email/#{user.id}/#{user.info.confirmation_token}") + + assert 302 == conn.status + end + + test "it confirms the user account", %{conn: conn, user: user} do + get(conn, "/api/account/confirm_email/#{user.id}/#{user.info.confirmation_token}") + + user = User.get_by_id(user.id) + + refute user.info.confirmation_pending + refute user.info.confirmation_token + end + + test "it returns 500 if user cannot be found by id", %{conn: conn, user: user} do + conn = get(conn, "/api/account/confirm_email/0/#{user.info.confirmation_token}") + + assert 500 == conn.status + end + + test "it returns 500 if token is invalid", %{conn: conn, user: user} do + conn = get(conn, "/api/account/confirm_email/#{user.id}/wrong_token") + + assert 500 == conn.status + end + end + + describe "POST /api/account/resend_confirmation_email" do + setup do + setting = Pleroma.Config.get([:instance, :account_activation_required]) + + unless setting do + Pleroma.Config.put([:instance, :account_activation_required], true) + on_exit(fn -> Pleroma.Config.put([:instance, :account_activation_required], setting) end) + end + + user = insert(:user) + info_change = User.Info.confirmation_changeset(user.info, :unconfirmed) + + {:ok, user} = + user + |> Changeset.change() + |> Changeset.put_embed(:info, info_change) + |> Repo.update() + + assert user.info.confirmation_pending + + [user: user] + end + + test "it returns 204 No Content", %{conn: conn, user: user} do + conn + |> assign(:user, user) + |> post("/api/account/resend_confirmation_email?email=#{user.email}") + |> json_response(:no_content) + end + + test "it sends confirmation email", %{conn: conn, user: user} do + conn + |> assign(:user, user) + |> post("/api/account/resend_confirmation_email?email=#{user.email}") + + email = Pleroma.Emails.UserEmail.account_confirmation_email(user) + notify_email = Pleroma.Config.get([:instance, :notify_email]) + instance_name = Pleroma.Config.get([:instance, :name]) + + assert_email_sent( + from: {instance_name, notify_email}, + to: {user.name, user.email}, + html_body: email.html_body + ) + end + end + describe "GET /api/externalprofile/show" do test "it returns the user", %{conn: conn} do user = insert(:user) @@ -861,6 +1215,112 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do result = json_response(conn, 200) assert Enum.sort(expected) == Enum.sort(result) end + + test "it returns 20 followers per page", %{conn: conn} do + user = insert(:user) + followers = insert_list(21, :user) + + Enum.each(followers, fn follower -> + User.follow(follower, user) + end) + + res_conn = + conn + |> assign(:user, user) + |> get("/api/statuses/followers") + + result = json_response(res_conn, 200) + assert length(result) == 20 + + res_conn = + conn + |> assign(:user, user) + |> get("/api/statuses/followers?page=2") + + result = json_response(res_conn, 200) + assert length(result) == 1 + end + + test "it returns a given user's followers with user_id", %{conn: conn} do + user = insert(:user) + follower_one = insert(:user) + follower_two = insert(:user) + not_follower = insert(:user) + + {:ok, follower_one} = User.follow(follower_one, user) + {:ok, follower_two} = User.follow(follower_two, user) + + conn = + conn + |> assign(:user, not_follower) + |> get("/api/statuses/followers", %{"user_id" => user.id}) + + assert MapSet.equal?( + MapSet.new(json_response(conn, 200)), + MapSet.new( + UserView.render("index.json", %{ + users: [follower_one, follower_two], + for: not_follower + }) + ) + ) + end + + test "it returns empty when hide_followers is set to true", %{conn: conn} do + user = insert(:user, %{info: %{hide_followers: true}}) + follower_one = insert(:user) + follower_two = insert(:user) + not_follower = insert(:user) + + {:ok, _follower_one} = User.follow(follower_one, user) + {:ok, _follower_two} = User.follow(follower_two, user) + + response = + conn + |> assign(:user, not_follower) + |> get("/api/statuses/followers", %{"user_id" => user.id}) + |> json_response(200) + + assert [] == response + end + + test "it returns the followers when hide_followers is set to true if requested by the user themselves", + %{ + conn: conn + } do + user = insert(:user, %{info: %{hide_followers: true}}) + follower_one = insert(:user) + follower_two = insert(:user) + _not_follower = insert(:user) + + {:ok, _follower_one} = User.follow(follower_one, user) + {:ok, _follower_two} = User.follow(follower_two, user) + + conn = + conn + |> assign(:user, user) + |> get("/api/statuses/followers", %{"user_id" => user.id}) + + refute [] == json_response(conn, 200) + end + end + + describe "GET /api/statuses/blocks" do + test "it returns the list of users blocked by requester", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, user} = User.block(user, other_user) + + conn = + conn + |> assign(:user, user) + |> get("/api/statuses/blocks") + + expected = UserView.render("index.json", %{users: [other_user], for: user}) + result = json_response(conn, 200) + assert Enum.sort(expected) == Enum.sort(result) + end end describe "GET /api/statuses/friends" do @@ -883,6 +1343,40 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do assert Enum.sort(expected) == Enum.sort(result) end + test "it returns 20 friends per page, except if 'export' is set to true", %{conn: conn} do + user = insert(:user) + followeds = insert_list(21, :user) + + {:ok, user} = + Enum.reduce(followeds, {:ok, user}, fn followed, {:ok, user} -> + User.follow(user, followed) + end) + + res_conn = + conn + |> assign(:user, user) + |> get("/api/statuses/friends") + + result = json_response(res_conn, 200) + assert length(result) == 20 + + res_conn = + conn + |> assign(:user, user) + |> get("/api/statuses/friends", %{page: 2}) + + result = json_response(res_conn, 200) + assert length(result) == 1 + + res_conn = + conn + |> assign(:user, user) + |> get("/api/statuses/friends", %{all: true}) + + result = json_response(res_conn, 200) + assert length(result) == 21 + end + test "it returns a given user's friends with user_id", %{conn: conn} do user = insert(:user) followed_one = insert(:user) @@ -905,6 +1399,44 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do ) end + test "it returns empty when hide_follows is set to true", %{conn: conn} do + user = insert(:user, %{info: %{hide_follows: true}}) + followed_one = insert(:user) + followed_two = insert(:user) + not_followed = insert(:user) + + {:ok, user} = User.follow(user, followed_one) + {:ok, user} = User.follow(user, followed_two) + + conn = + conn + |> assign(:user, not_followed) + |> get("/api/statuses/friends", %{"user_id" => user.id}) + + assert [] == json_response(conn, 200) + end + + test "it returns friends when hide_follows is set to true if the user themselves request it", + %{ + conn: conn + } do + user = insert(:user, %{info: %{hide_follows: true}}) + followed_one = insert(:user) + followed_two = insert(:user) + _not_followed = insert(:user) + + {:ok, _user} = User.follow(user, followed_one) + {:ok, _user} = User.follow(user, followed_two) + + response = + conn + |> assign(:user, user) + |> get("/api/statuses/friends", %{"user_id" => user.id}) + |> json_response(200) + + refute [] == response + end + test "it returns a given user's friends with screen_name", %{conn: conn} do user = insert(:user) followed_one = insert(:user) @@ -969,11 +1501,85 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do assert user.name == "new name" assert user.bio == - "hi <span><a class='mention' href='#{user2.ap_id}'>@<span>#{user2.nickname}</span></a></span>" + "hi <span class='h-card'><a data-user='#{user2.id}' class='u-url mention' href='#{ + user2.ap_id + }'>@<span>#{user2.nickname}</span></a></span>" assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user}) end + test "it sets and un-sets hide_follows", %{conn: conn} do + user = insert(:user) + + conn + |> assign(:user, user) + |> post("/api/account/update_profile.json", %{ + "hide_follows" => "true" + }) + + user = Repo.get!(User, user.id) + assert user.info.hide_follows == true + + conn = + conn + |> assign(:user, user) + |> post("/api/account/update_profile.json", %{ + "hide_follows" => "false" + }) + + user = Repo.get!(User, user.id) + assert user.info.hide_follows == false + assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user}) + end + + test "it sets and un-sets hide_followers", %{conn: conn} do + user = insert(:user) + + conn + |> assign(:user, user) + |> post("/api/account/update_profile.json", %{ + "hide_followers" => "true" + }) + + user = Repo.get!(User, user.id) + assert user.info.hide_followers == true + + conn = + conn + |> assign(:user, user) + |> post("/api/account/update_profile.json", %{ + "hide_followers" => "false" + }) + + user = Repo.get!(User, user.id) + assert user.info.hide_followers == false + assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user}) + end + + test "it sets and un-sets show_role", %{conn: conn} do + user = insert(:user) + + conn + |> assign(:user, user) + |> post("/api/account/update_profile.json", %{ + "show_role" => "true" + }) + + user = Repo.get!(User, user.id) + assert user.info.show_role == true + + conn = + conn + |> assign(:user, user) + |> post("/api/account/update_profile.json", %{ + "show_role" => "false" + }) + + user = Repo.get!(User, user.id) + assert user.info.show_role == false + assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user}) + end + test "it locks an account", %{conn: conn} do user = insert(:user) @@ -1136,7 +1742,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do }) assert json_response(conn, 200) == %{"status" => "success"} - fetched_user = Repo.get(User, current_user.id) + fetched_user = User.get_by_id(current_user.id) assert Pbkdf2.checkpw("newpass", fetched_user.password_hash) == true end end @@ -1177,8 +1783,8 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do {:ok, _activity} = ActivityPub.follow(other_user, user) - user = Repo.get(User, user.id) - other_user = Repo.get(User, other_user.id) + user = User.get_by_id(user.id) + other_user = User.get_by_id(other_user.id) assert User.following?(other_user, user) == false @@ -1190,6 +1796,24 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do assert [relationship] = json_response(conn, 200) assert other_user.id == relationship["id"] end + + test "requires 'read' permission", %{conn: conn} do + token1 = insert(:oauth_token, scopes: ["write"]) + token2 = insert(:oauth_token, scopes: ["read"]) + + for token <- [token1, token2] do + conn = + conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> get("/api/pleroma/friend_requests") + + if token == token1 do + assert %{"error" => "Insufficient permissions: read."} == json_response(conn, 403) + else + assert json_response(conn, 200) + end + end + end end describe "POST /api/pleroma/friendships/approve" do @@ -1199,15 +1823,15 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do {:ok, _activity} = ActivityPub.follow(other_user, user) - user = Repo.get(User, user.id) - other_user = Repo.get(User, other_user.id) + user = User.get_by_id(user.id) + other_user = User.get_by_id(other_user.id) assert User.following?(other_user, user) == false conn = build_conn() |> assign(:user, user) - |> post("/api/pleroma/friendships/approve", %{"user_id" => to_string(other_user.id)}) + |> post("/api/pleroma/friendships/approve", %{"user_id" => other_user.id}) assert relationship = json_response(conn, 200) assert other_user.id == relationship["id"] @@ -1222,15 +1846,15 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do {:ok, _activity} = ActivityPub.follow(other_user, user) - user = Repo.get(User, user.id) - other_user = Repo.get(User, other_user.id) + user = User.get_by_id(user.id) + other_user = User.get_by_id(other_user.id) assert User.following?(other_user, user) == false conn = build_conn() |> assign(:user, user) - |> post("/api/pleroma/friendships/deny", %{"user_id" => to_string(other_user.id)}) + |> post("/api/pleroma/friendships/deny", %{"user_id" => other_user.id}) assert relationship = json_response(conn, 200) assert other_user.id == relationship["id"] @@ -1241,16 +1865,205 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do describe "GET /api/pleroma/search_user" do test "it returns users, ordered by similarity", %{conn: conn} do user = insert(:user, %{name: "eal"}) - user_two = insert(:user, %{name: "ean"}) - user_three = insert(:user, %{name: "ebn"}) + user_two = insert(:user, %{name: "eal me"}) + _user_three = insert(:user, %{name: "zzz"}) + + resp = + conn + |> get(twitter_api_search__path(conn, :search_user), query: "eal me") + |> json_response(200) + + assert length(resp) == 2 + assert [user_two.id, user.id] == Enum.map(resp, fn %{"id" => id} -> id end) + end + end + + describe "POST /api/media/upload" do + setup context do + Pleroma.DataCase.ensure_local_uploader(context) + end + + test "it performs the upload and sets `data[actor]` with AP id of uploader user", %{ + conn: conn + } do + user = insert(:user) + + upload_filename = "test/fixtures/image_tmp.jpg" + File.cp!("test/fixtures/image.jpg", upload_filename) + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname(upload_filename), + filename: "image.jpg" + } + + response = + conn + |> assign(:user, user) + |> put_req_header("content-type", "application/octet-stream") + |> post("/api/media/upload", %{ + "media" => file + }) + |> json_response(:ok) + + assert response["media_id"] + object = Repo.get(Object, response["media_id"]) + assert object + assert object.data["actor"] == User.ap_id(user) + end + end + + describe "POST /api/media/metadata/create" do + setup do + object = insert(:note) + user = User.get_by_ap_id(object.data["actor"]) + %{object: object, user: user} + end + + test "it returns :forbidden status on attempt to modify someone else's upload", %{ + conn: conn, + object: object + } do + initial_description = object.data["name"] + another_user = insert(:user) + + conn + |> assign(:user, another_user) + |> post("/api/media/metadata/create", %{"media_id" => object.id}) + |> json_response(:forbidden) + + object = Repo.get(Object, object.id) + assert object.data["name"] == initial_description + end + + test "it updates `data[name]` of referenced Object with provided value", %{ + conn: conn, + object: object, + user: user + } do + description = "Informative description of the image. Initial value: #{object.data["name"]}}" + + conn + |> assign(:user, user) + |> post("/api/media/metadata/create", %{ + "media_id" => object.id, + "alt_text" => %{"text" => description} + }) + |> json_response(:no_content) + + object = Repo.get(Object, object.id) + assert object.data["name"] == description + end + end + + describe "POST /api/statuses/user_timeline.json?user_id=:user_id&pinned=true" do + test "it returns a list of pinned statuses", %{conn: conn} do + Pleroma.Config.put([:instance, :max_pinned_statuses], 1) + + user = insert(:user, %{name: "egor"}) + {:ok, %{id: activity_id}} = CommonAPI.post(user, %{"status" => "HI!!!"}) + {:ok, _} = CommonAPI.pin(activity_id, user) resp = conn - |> get(twitter_api_search__path(conn, :search_user), query: "eal") + |> get("/api/statuses/user_timeline.json", %{user_id: user.id, pinned: true}) |> json_response(200) - assert length(resp) == 3 - assert [user.id, user_two.id, user_three.id] == Enum.map(resp, fn %{"id" => id} -> id end) + assert length(resp) == 1 + assert [%{"id" => ^activity_id, "pinned" => true}] = resp + end + end + + describe "POST /api/statuses/pin/:id" do + setup do + Pleroma.Config.put([:instance, :max_pinned_statuses], 1) + [user: insert(:user)] + end + + test "without valid credentials", %{conn: conn} do + note_activity = insert(:note_activity) + conn = post(conn, "/api/statuses/pin/#{note_activity.id}.json") + assert json_response(conn, 403) == %{"error" => "Invalid credentials."} + end + + test "with credentials", %{conn: conn, user: user} do + {:ok, activity} = CommonAPI.post(user, %{"status" => "test!"}) + + request_path = "/api/statuses/pin/#{activity.id}.json" + + response = + conn + |> with_credentials(user.nickname, "test") + |> post(request_path) + + user = refresh_record(user) + + assert json_response(response, 200) == + ActivityView.render("activity.json", %{user: user, for: user, activity: activity}) + end + end + + describe "POST /api/statuses/unpin/:id" do + setup do + Pleroma.Config.put([:instance, :max_pinned_statuses], 1) + [user: insert(:user)] + end + + test "without valid credentials", %{conn: conn} do + note_activity = insert(:note_activity) + conn = post(conn, "/api/statuses/unpin/#{note_activity.id}.json") + assert json_response(conn, 403) == %{"error" => "Invalid credentials."} + end + + test "with credentials", %{conn: conn, user: user} do + {:ok, activity} = CommonAPI.post(user, %{"status" => "test!"}) + {:ok, activity} = CommonAPI.pin(activity.id, user) + + request_path = "/api/statuses/unpin/#{activity.id}.json" + + response = + conn + |> with_credentials(user.nickname, "test") + |> post(request_path) + + user = refresh_record(user) + + assert json_response(response, 200) == + ActivityView.render("activity.json", %{user: user, for: user, activity: activity}) + end + end + + describe "GET /api/oauth_tokens" do + setup do + token = insert(:oauth_token) |> Repo.preload(:user) + + %{token: token} + end + + test "renders list", %{token: token} do + response = + build_conn() + |> assign(:user, token.user) + |> get("/api/oauth_tokens") + + keys = + json_response(response, 200) + |> hd() + |> Map.keys() + + assert keys -- ["id", "app_name", "valid_until"] == [] + end + + test "revoke token", %{token: token} do + response = + build_conn() + |> assign(:user, token.user) + |> delete("/api/oauth_tokens/#{token.id}") + + tokens = Token.get_user_tokens(token.user) + + assert tokens == [] + assert response.status == 201 end end end diff --git a/test/web/twitter_api/twitter_api_test.exs b/test/web/twitter_api/twitter_api_test.exs index bc53fe68a..5bea1037a 100644 --- a/test/web/twitter_api/twitter_api_test.exs +++ b/test/web/twitter_api/twitter_api_test.exs @@ -1,16 +1,29 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do use Pleroma.DataCase - alias Pleroma.Builders.UserBuilder - alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView} - alias Pleroma.{Activity, User, Object, Repo, UserInviteToken} + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.UserInviteToken alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.TwitterAPI.ActivityView + alias Pleroma.Web.TwitterAPI.TwitterAPI + alias Pleroma.Web.TwitterAPI.UserView import Pleroma.Factory + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + test "create a status" do user = insert(:user) - _mentioned_user = insert(:user, %{nickname: "shp", ap_id: "shp"}) + mentioned_user = insert(:user, %{nickname: "shp", ap_id: "shp"}) object_data = %{ "type" => "Image", @@ -36,7 +49,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do object = Object.normalize(activity.data["object"]) expected_text = - "Hello again, <span><a class='mention' href='shp'>@<span>shp</span></a></span>.<script></script><br>This is on another :moominmamma: line. <a href='http://localhost:4001/tag/2hu' rel='tag'>#2hu</a> <a href='http://localhost:4001/tag/epic' rel='tag'>#epic</a> <a href='http://localhost:4001/tag/phantasmagoric' rel='tag'>#phantasmagoric</a><br><a href=\"http://example.org/image.jpg\" class='attachment'>image.jpg</a>" + "Hello again, <span class='h-card'><a data-user='#{mentioned_user.id}' class='u-url mention' href='shp'>@<span>shp</span></a></span>.<script></script><br>This is on another :moominmamma: line. <a class='hashtag' data-tag='2hu' href='http://localhost:4001/tag/2hu' rel='tag'>#2hu</a> <a class='hashtag' data-tag='epic' href='http://localhost:4001/tag/epic' rel='tag'>#epic</a> <a class='hashtag' data-tag='phantasmagoric' href='http://localhost:4001/tag/phantasmagoric' rel='tag'>#phantasmagoric</a><br><a href=\"http://example.org/image.jpg\" class='attachment'>image.jpg</a>" assert get_in(object.data, ["content"]) == expected_text assert get_in(object.data, ["type"]) == "Note" @@ -94,7 +107,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do assert get_in(reply_object.data, ["context"]) == get_in(object.data, ["context"]) assert get_in(reply_object.data, ["inReplyTo"]) == get_in(activity.data, ["object"]) - assert get_in(reply_object.data, ["inReplyToStatusId"]) == activity.id + assert Activity.get_in_reply_to_activity(reply).id == activity.id end test "Follow another user using user_id" do @@ -184,25 +197,42 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do end test "upload a file" do + user = insert(:user) + file = %Plug.Upload{ content_type: "image/jpg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } - response = TwitterAPI.upload(file) + response = TwitterAPI.upload(file, user) assert is_binary(response) end test "it favorites a status, returns the updated activity" do user = insert(:user) + other_user = insert(:user) note_activity = insert(:note_activity) {:ok, status} = TwitterAPI.fav(user, note_activity.id) updated_activity = Activity.get_by_ap_id(note_activity.data["id"]) + assert ActivityView.render("activity.json", %{activity: updated_activity})["fave_num"] == 1 + + object = Object.normalize(note_activity.data["object"]) + + assert object.data["like_count"] == 1 assert status == updated_activity + + {:ok, _status} = TwitterAPI.fav(other_user, note_activity.id) + + object = Object.normalize(note_activity.data["object"]) + + assert object.data["like_count"] == 2 + + updated_activity = Activity.get_by_ap_id(note_activity.data["id"]) + assert ActivityView.render("activity.json", %{activity: updated_activity})["fave_num"] == 2 end test "it unfavorites a status, returns the updated activity" do @@ -246,19 +276,69 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do "nickname" => "lain", "email" => "lain@wired.jp", "fullname" => "lain iwakura", - "bio" => "close the world.", "password" => "bear", "confirm" => "bear" } {:ok, user} = TwitterAPI.register_user(data) - fetched_user = Repo.get_by(User, nickname: "lain") + fetched_user = User.get_by_nickname("lain") assert UserView.render("show.json", %{user: user}) == UserView.render("show.json", %{user: fetched_user}) end + test "it registers a new user with empty string in bio and returns the user." do + data = %{ + "nickname" => "lain", + "email" => "lain@wired.jp", + "fullname" => "lain iwakura", + "bio" => "", + "password" => "bear", + "confirm" => "bear" + } + + {:ok, user} = TwitterAPI.register_user(data) + + fetched_user = User.get_by_nickname("lain") + + assert UserView.render("show.json", %{user: user}) == + UserView.render("show.json", %{user: fetched_user}) + end + + test "it sends confirmation email if :account_activation_required is specified in instance config" do + setting = Pleroma.Config.get([:instance, :account_activation_required]) + + unless setting do + Pleroma.Config.put([:instance, :account_activation_required], true) + on_exit(fn -> Pleroma.Config.put([:instance, :account_activation_required], setting) end) + end + + data = %{ + "nickname" => "lain", + "email" => "lain@wired.jp", + "fullname" => "lain iwakura", + "bio" => "", + "password" => "bear", + "confirm" => "bear" + } + + {:ok, user} = TwitterAPI.register_user(data) + + assert user.info.confirmation_pending + + email = Pleroma.Emails.UserEmail.account_confirmation_email(user) + + notify_email = Pleroma.Config.get([:instance, :notify_email]) + instance_name = Pleroma.Config.get([:instance, :name]) + + Swoosh.TestAssertions.assert_email_sent( + from: {instance_name, notify_email}, + to: {user.name, user.email}, + html_body: email.html_body + ) + end + test "it registers a new user and parses mentions in the bio" do data1 = %{ "nickname" => "john", @@ -283,73 +363,318 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do {:ok, user2} = TwitterAPI.register_user(data2) expected_text = - "<span><a class='mention' href='#{user1.ap_id}'>@<span>john</span></a></span> test" + "<span class='h-card'><a data-user='#{user1.id}' class='u-url mention' href='#{user1.ap_id}'>@<span>john</span></a></span> test" assert user2.bio == expected_text end - @moduletag skip: "needs 'registrations_open: false' in config" - test "it registers a new user via invite token and returns the user." do - {:ok, token} = UserInviteToken.create_token() + describe "register with one time token" do + setup do + setting = Pleroma.Config.get([:instance, :registrations_open]) - data = %{ - "nickname" => "vinny", - "email" => "pasta@pizza.vs", - "fullname" => "Vinny Vinesauce", - "bio" => "streamer", - "password" => "hiptofbees", - "confirm" => "hiptofbees", - "token" => token.token - } + if setting do + Pleroma.Config.put([:instance, :registrations_open], false) + on_exit(fn -> Pleroma.Config.put([:instance, :registrations_open], setting) end) + end - {:ok, user} = TwitterAPI.register_user(data) + :ok + end - fetched_user = Repo.get_by(User, nickname: "vinny") - token = Repo.get_by(UserInviteToken, token: token.token) + test "returns user on success" do + {:ok, invite} = UserInviteToken.create_invite() - assert token.used == true + data = %{ + "nickname" => "vinny", + "email" => "pasta@pizza.vs", + "fullname" => "Vinny Vinesauce", + "bio" => "streamer", + "password" => "hiptofbees", + "confirm" => "hiptofbees", + "token" => invite.token + } - assert UserView.render("show.json", %{user: user}) == - UserView.render("show.json", %{user: fetched_user}) + {:ok, user} = TwitterAPI.register_user(data) + + fetched_user = User.get_by_nickname("vinny") + invite = Repo.get_by(UserInviteToken, token: invite.token) + + assert invite.used == true + + assert UserView.render("show.json", %{user: user}) == + UserView.render("show.json", %{user: fetched_user}) + end + + test "returns error on invalid token" do + data = %{ + "nickname" => "GrimReaper", + "email" => "death@reapers.afterlife", + "fullname" => "Reaper Grim", + "bio" => "Your time has come", + "password" => "scythe", + "confirm" => "scythe", + "token" => "DudeLetMeInImAFairy" + } + + {:error, msg} = TwitterAPI.register_user(data) + + assert msg == "Invalid token" + refute User.get_by_nickname("GrimReaper") + end + + test "returns error on expired token" do + {:ok, invite} = UserInviteToken.create_invite() + UserInviteToken.update_invite!(invite, used: true) + + data = %{ + "nickname" => "GrimReaper", + "email" => "death@reapers.afterlife", + "fullname" => "Reaper Grim", + "bio" => "Your time has come", + "password" => "scythe", + "confirm" => "scythe", + "token" => invite.token + } + + {:error, msg} = TwitterAPI.register_user(data) + + assert msg == "Expired token" + refute User.get_by_nickname("GrimReaper") + end end - @moduletag skip: "needs 'registrations_open: false' in config" - test "it returns an error if invalid token submitted" do - data = %{ - "nickname" => "GrimReaper", - "email" => "death@reapers.afterlife", - "fullname" => "Reaper Grim", - "bio" => "Your time has come", - "password" => "scythe", - "confirm" => "scythe", - "token" => "DudeLetMeInImAFairy" - } + describe "registers with date limited token" do + setup do + setting = Pleroma.Config.get([:instance, :registrations_open]) + + if setting do + Pleroma.Config.put([:instance, :registrations_open], false) + on_exit(fn -> Pleroma.Config.put([:instance, :registrations_open], setting) end) + end + + data = %{ + "nickname" => "vinny", + "email" => "pasta@pizza.vs", + "fullname" => "Vinny Vinesauce", + "bio" => "streamer", + "password" => "hiptofbees", + "confirm" => "hiptofbees" + } + + check_fn = fn invite -> + data = Map.put(data, "token", invite.token) + {:ok, user} = TwitterAPI.register_user(data) + fetched_user = User.get_by_nickname("vinny") + + assert UserView.render("show.json", %{user: user}) == + UserView.render("show.json", %{user: fetched_user}) + end + + {:ok, data: data, check_fn: check_fn} + end + + test "returns user on success", %{check_fn: check_fn} do + {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today()}) + + check_fn.(invite) + + invite = Repo.get_by(UserInviteToken, token: invite.token) + + refute invite.used + end + + test "returns user on token which expired tomorrow", %{check_fn: check_fn} do + {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), 1)}) + + check_fn.(invite) + + invite = Repo.get_by(UserInviteToken, token: invite.token) + + refute invite.used + end + + test "returns an error on overdue date", %{data: data} do + {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1)}) - {:error, msg} = TwitterAPI.register_user(data) + data = Map.put(data, "token", invite.token) - assert msg == "Invalid token" - refute Repo.get_by(User, nickname: "GrimReaper") + {:error, msg} = TwitterAPI.register_user(data) + + assert msg == "Expired token" + refute User.get_by_nickname("vinny") + invite = Repo.get_by(UserInviteToken, token: invite.token) + + refute invite.used + end end - @moduletag skip: "needs 'registrations_open: false' in config" - test "it returns an error if expired token submitted" do - {:ok, token} = UserInviteToken.create_token() - UserInviteToken.mark_as_used(token.token) + describe "registers with reusable token" do + setup do + setting = Pleroma.Config.get([:instance, :registrations_open]) - data = %{ - "nickname" => "GrimReaper", - "email" => "death@reapers.afterlife", - "fullname" => "Reaper Grim", - "bio" => "Your time has come", - "password" => "scythe", - "confirm" => "scythe", - "token" => token.token - } + if setting do + Pleroma.Config.put([:instance, :registrations_open], false) + on_exit(fn -> Pleroma.Config.put([:instance, :registrations_open], setting) end) + end + + :ok + end + + test "returns user on success, after him registration fails" do + {:ok, invite} = UserInviteToken.create_invite(%{max_use: 100}) + + UserInviteToken.update_invite!(invite, uses: 99) + + data = %{ + "nickname" => "vinny", + "email" => "pasta@pizza.vs", + "fullname" => "Vinny Vinesauce", + "bio" => "streamer", + "password" => "hiptofbees", + "confirm" => "hiptofbees", + "token" => invite.token + } + + {:ok, user} = TwitterAPI.register_user(data) + fetched_user = User.get_by_nickname("vinny") + invite = Repo.get_by(UserInviteToken, token: invite.token) + + assert invite.used == true + + assert UserView.render("show.json", %{user: user}) == + UserView.render("show.json", %{user: fetched_user}) + + data = %{ + "nickname" => "GrimReaper", + "email" => "death@reapers.afterlife", + "fullname" => "Reaper Grim", + "bio" => "Your time has come", + "password" => "scythe", + "confirm" => "scythe", + "token" => invite.token + } + + {:error, msg} = TwitterAPI.register_user(data) - {:error, msg} = TwitterAPI.register_user(data) + assert msg == "Expired token" + refute User.get_by_nickname("GrimReaper") + end + end + + describe "registers with reusable date limited token" do + setup do + setting = Pleroma.Config.get([:instance, :registrations_open]) + + if setting do + Pleroma.Config.put([:instance, :registrations_open], false) + on_exit(fn -> Pleroma.Config.put([:instance, :registrations_open], setting) end) + end + + :ok + end + + test "returns user on success" do + {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 100}) + + data = %{ + "nickname" => "vinny", + "email" => "pasta@pizza.vs", + "fullname" => "Vinny Vinesauce", + "bio" => "streamer", + "password" => "hiptofbees", + "confirm" => "hiptofbees", + "token" => invite.token + } + + {:ok, user} = TwitterAPI.register_user(data) + fetched_user = User.get_by_nickname("vinny") + invite = Repo.get_by(UserInviteToken, token: invite.token) + + refute invite.used + + assert UserView.render("show.json", %{user: user}) == + UserView.render("show.json", %{user: fetched_user}) + end + + test "error after max uses" do + {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 100}) + + UserInviteToken.update_invite!(invite, uses: 99) + + data = %{ + "nickname" => "vinny", + "email" => "pasta@pizza.vs", + "fullname" => "Vinny Vinesauce", + "bio" => "streamer", + "password" => "hiptofbees", + "confirm" => "hiptofbees", + "token" => invite.token + } + + {:ok, user} = TwitterAPI.register_user(data) + fetched_user = User.get_by_nickname("vinny") + invite = Repo.get_by(UserInviteToken, token: invite.token) + assert invite.used == true + + assert UserView.render("show.json", %{user: user}) == + UserView.render("show.json", %{user: fetched_user}) + + data = %{ + "nickname" => "GrimReaper", + "email" => "death@reapers.afterlife", + "fullname" => "Reaper Grim", + "bio" => "Your time has come", + "password" => "scythe", + "confirm" => "scythe", + "token" => invite.token + } + + {:error, msg} = TwitterAPI.register_user(data) + + assert msg == "Expired token" + refute User.get_by_nickname("GrimReaper") + end + + test "returns error on overdue date" do + {:ok, invite} = + UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1), max_use: 100}) + + data = %{ + "nickname" => "GrimReaper", + "email" => "death@reapers.afterlife", + "fullname" => "Reaper Grim", + "bio" => "Your time has come", + "password" => "scythe", + "confirm" => "scythe", + "token" => invite.token + } - assert msg == "Expired token" - refute Repo.get_by(User, nickname: "GrimReaper") + {:error, msg} = TwitterAPI.register_user(data) + + assert msg == "Expired token" + refute User.get_by_nickname("GrimReaper") + end + + test "returns error on with overdue date and after max" do + {:ok, invite} = + UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1), max_use: 100}) + + UserInviteToken.update_invite!(invite, uses: 100) + + data = %{ + "nickname" => "GrimReaper", + "email" => "death@reapers.afterlife", + "fullname" => "Reaper Grim", + "bio" => "Your time has come", + "password" => "scythe", + "confirm" => "scythe", + "token" => invite.token + } + + {:error, msg} = TwitterAPI.register_user(data) + + assert msg == "Expired token" + refute User.get_by_nickname("GrimReaper") + end end test "it returns the error on registration problems" do @@ -364,7 +689,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do {:error, error_object} = TwitterAPI.register_user(data) assert is_binary(error_object[:error]) - refute Repo.get_by(User, nickname: "lain") + refute User.get_by_nickname("lain") end test "it assigns an integer conversation_id" do @@ -380,22 +705,6 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do :ok end - describe "context_to_conversation_id" do - test "creates a mapping object" do - conversation_id = TwitterAPI.context_to_conversation_id("random context") - object = Object.get_by_ap_id("random context") - - assert conversation_id == object.id - end - - test "returns an existing mapping for an existing object" do - {:ok, object} = Object.context_mapping("random context") |> Repo.insert() - conversation_id = TwitterAPI.context_to_conversation_id("random context") - - assert conversation_id == object.id - end - end - describe "fetching a user by uri" do test "fetches a user by uri" do id = "https://mastodon.social/users/lambadalambda" @@ -406,7 +715,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do assert represented["id"] == UserView.render("show.json", %{user: remote, for: user})["id"] # Also fetches the feed. - # assert Activity.get_create_activity_by_object_ap_id("tag:mastodon.social,2017-04-05:objectId=1641750:objectType=Status") + # assert Activity.get_create_by_object_ap_id("tag:mastodon.social,2017-04-05:objectId=1641750:objectType=Status") + # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength end end end diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs new file mode 100644 index 000000000..c58b49ea4 --- /dev/null +++ b/test/web/twitter_api/util_controller_test.exs @@ -0,0 +1,248 @@ +defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Notification + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.CommonAPI + import Pleroma.Factory + + setup do + Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + describe "POST /api/pleroma/follow_import" do + test "it returns HTTP 200", %{conn: conn} do + user1 = insert(:user) + user2 = insert(:user) + + response = + conn + |> assign(:user, user1) + |> post("/api/pleroma/follow_import", %{"list" => "#{user2.ap_id}"}) + |> json_response(:ok) + + assert response == "job started" + end + + test "it imports new-style mastodon follow lists", %{conn: conn} do + user1 = insert(:user) + user2 = insert(:user) + + response = + conn + |> assign(:user, user1) + |> post("/api/pleroma/follow_import", %{ + "list" => "Account address,Show boosts\n#{user2.ap_id},true" + }) + |> json_response(:ok) + + assert response == "job started" + end + + test "requires 'follow' permission", %{conn: conn} do + token1 = insert(:oauth_token, scopes: ["read", "write"]) + token2 = insert(:oauth_token, scopes: ["follow"]) + another_user = insert(:user) + + for token <- [token1, token2] do + conn = + conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> post("/api/pleroma/follow_import", %{"list" => "#{another_user.ap_id}"}) + + if token == token1 do + assert %{"error" => "Insufficient permissions: follow."} == json_response(conn, 403) + else + assert json_response(conn, 200) + end + end + end + end + + describe "POST /api/pleroma/blocks_import" do + test "it returns HTTP 200", %{conn: conn} do + user1 = insert(:user) + user2 = insert(:user) + + response = + conn + |> assign(:user, user1) + |> post("/api/pleroma/blocks_import", %{"list" => "#{user2.ap_id}"}) + |> json_response(:ok) + + assert response == "job started" + end + end + + describe "POST /api/pleroma/notifications/read" do + test "it marks a single notification as read", %{conn: conn} do + user1 = insert(:user) + user2 = insert(:user) + {:ok, activity1} = CommonAPI.post(user2, %{"status" => "hi @#{user1.nickname}"}) + {:ok, activity2} = CommonAPI.post(user2, %{"status" => "hi @#{user1.nickname}"}) + {:ok, [notification1]} = Notification.create_notifications(activity1) + {:ok, [notification2]} = Notification.create_notifications(activity2) + + conn + |> assign(:user, user1) + |> post("/api/pleroma/notifications/read", %{"id" => "#{notification1.id}"}) + |> json_response(:ok) + + assert Repo.get(Notification, notification1.id).seen + refute Repo.get(Notification, notification2.id).seen + end + end + + describe "PUT /api/pleroma/notification_settings" do + test "it updates notification settings", %{conn: conn} do + user = insert(:user) + + conn + |> assign(:user, user) + |> put("/api/pleroma/notification_settings", %{ + "remote" => false, + "followers" => false, + "bar" => 1 + }) + |> json_response(:ok) + + user = Repo.get(User, user.id) + + assert %{"remote" => false, "local" => true, "followers" => false, "follows" => true} == + user.info.notification_settings + end + end + + describe "GET /api/statusnet/config.json" do + test "returns the state of safe_dm_mentions flag", %{conn: conn} do + option = Pleroma.Config.get([:instance, :safe_dm_mentions]) + Pleroma.Config.put([:instance, :safe_dm_mentions], true) + + response = + conn + |> get("/api/statusnet/config.json") + |> json_response(:ok) + + assert response["site"]["safeDMMentionsEnabled"] == "1" + + Pleroma.Config.put([:instance, :safe_dm_mentions], false) + + response = + conn + |> get("/api/statusnet/config.json") + |> json_response(:ok) + + assert response["site"]["safeDMMentionsEnabled"] == "0" + + Pleroma.Config.put([:instance, :safe_dm_mentions], option) + end + + test "it returns the managed config", %{conn: conn} do + Pleroma.Config.put([:instance, :managed_config], false) + Pleroma.Config.put([:fe], theme: "rei-ayanami-towel") + + response = + conn + |> get("/api/statusnet/config.json") + |> json_response(:ok) + + refute response["site"]["pleromafe"] + + Pleroma.Config.put([:instance, :managed_config], true) + + response = + conn + |> get("/api/statusnet/config.json") + |> json_response(:ok) + + assert response["site"]["pleromafe"] + end + + test "if :pleroma, :fe is false, it returns the new style config settings", %{conn: conn} do + Pleroma.Config.put([:instance, :managed_config], true) + Pleroma.Config.put([:fe, :theme], "rei-ayanami-towel") + Pleroma.Config.put([:frontend_configurations, :pleroma_fe], %{theme: "asuka-hospital"}) + + response = + conn + |> get("/api/statusnet/config.json") + |> json_response(:ok) + + assert response["site"]["pleromafe"]["theme"] == "rei-ayanami-towel" + + Pleroma.Config.put([:fe], false) + + response = + conn + |> get("/api/statusnet/config.json") + |> json_response(:ok) + + assert response["site"]["pleromafe"]["theme"] == "asuka-hospital" + end + end + + describe "GET /api/pleroma/frontend_configurations" do + test "returns everything in :pleroma, :frontend_configurations", %{conn: conn} do + config = [ + frontend_a: %{ + x: 1, + y: 2 + }, + frontend_b: %{ + z: 3 + } + ] + + Pleroma.Config.put(:frontend_configurations, config) + + response = + conn + |> get("/api/pleroma/frontend_configurations") + |> json_response(:ok) + + assert response == Jason.encode!(config |> Enum.into(%{})) |> Jason.decode!() + end + end + + describe "/api/pleroma/emoji" do + test "returns json with custom emoji with tags", %{conn: conn} do + emoji = + conn + |> get("/api/pleroma/emoji") + |> json_response(200) + + assert Enum.all?(emoji, fn + {_key, + %{ + "image_url" => url, + "tags" => tags + }} -> + is_binary(url) and is_list(tags) + end) + end + end + + describe "GET /ostatus_subscribe?acct=...." do + test "adds status to pleroma instance if the `acct` is a status", %{conn: conn} do + conn = + get( + conn, + "/ostatus_subscribe?acct=https://mastodon.social/users/emelie/statuses/101849165031453009" + ) + + assert redirected_to(conn) =~ "/notice/" + end + + test "show follow account page if the `acct` is a account link", %{conn: conn} do + response = + get( + conn, + "/ostatus_subscribe?acct=https://mastodon.social/users/emelie" + ) + + assert html_response(response, 200) =~ "Log in to follow" + end + end +end diff --git a/test/web/twitter_api/views/activity_view_test.exs b/test/web/twitter_api/views/activity_view_test.exs index f4741cf24..7ef0270cc 100644 --- a/test/web/twitter_api/views/activity_view_test.exs +++ b/test/web/twitter_api/views/activity_view_test.exs @@ -1,19 +1,126 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.TwitterAPI.ActivityViewTest do use Pleroma.DataCase + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.TwitterAPI.ActivityView alias Pleroma.Web.TwitterAPI.UserView - alias Pleroma.Web.TwitterAPI.TwitterAPI - alias Pleroma.Repo - alias Pleroma.{Activity, Object} - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub import Pleroma.Factory + import Tesla.Mock + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + import Mock + test "returns a temporary ap_id based user for activities missing db users" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"}) + + Repo.delete(user) + Cachex.clear(:user_cache) + + %{"user" => tw_user} = ActivityView.render("activity.json", activity: activity) + + assert tw_user["screen_name"] == "erroruser@example.com" + assert tw_user["name"] == user.ap_id + assert tw_user["statusnet_profile_url"] == user.ap_id + end + + test "tries to get a user by nickname if fetching by ap_id doesn't work" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"}) + + {:ok, user} = + user + |> Ecto.Changeset.change(%{ap_id: "#{user.ap_id}/extension/#{user.nickname}"}) + |> Repo.update() + + Cachex.clear(:user_cache) + + result = ActivityView.render("activity.json", activity: activity) + assert result["user"]["id"] == user.id + end + + test "tells if the message is muted for some reason" do + user = insert(:user) + other_user = insert(:user) + + {:ok, user} = User.mute(user, other_user) + + {:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"}) + status = ActivityView.render("activity.json", %{activity: activity}) + + assert status["muted"] == false + + status = ActivityView.render("activity.json", %{activity: activity, for: user}) + + assert status["muted"] == true + end + + test "a create activity with a html status" do + text = """ + #Bike log - Commute Tuesday\nhttps://pla.bike/posts/20181211/\n#cycling #CHScycling #commute\nMVIMG_20181211_054020.jpg + """ + + {:ok, activity} = CommonAPI.post(insert(:user), %{"status" => text}) + + result = ActivityView.render("activity.json", activity: activity) + + assert result["statusnet_html"] == + "<a class=\"hashtag\" data-tag=\"bike\" href=\"http://localhost:4001/tag/bike\" rel=\"tag\">#Bike</a> log - Commute Tuesday<br /><a href=\"https://pla.bike/posts/20181211/\">https://pla.bike/posts/20181211/</a><br /><a class=\"hashtag\" data-tag=\"cycling\" href=\"http://localhost:4001/tag/cycling\" rel=\"tag\">#cycling</a> <a class=\"hashtag\" data-tag=\"chscycling\" href=\"http://localhost:4001/tag/chscycling\" rel=\"tag\">#CHScycling</a> <a class=\"hashtag\" data-tag=\"commute\" href=\"http://localhost:4001/tag/commute\" rel=\"tag\">#commute</a><br />MVIMG_20181211_054020.jpg" + + assert result["text"] == + "#Bike log - Commute Tuesday\nhttps://pla.bike/posts/20181211/\n#cycling #CHScycling #commute\nMVIMG_20181211_054020.jpg" + end + + test "a create activity with a summary containing emoji" do + {:ok, activity} = + CommonAPI.post(insert(:user), %{ + "spoiler_text" => ":woollysocks: meow", + "status" => "." + }) + + result = ActivityView.render("activity.json", activity: activity) + + expected = ":woollysocks: meow" + + expected_html = + "<img height=\"32px\" width=\"32px\" alt=\"woollysocks\" title=\"woollysocks\" src=\"http://localhost:4001/finmoji/128px/woollysocks-128.png\" /> meow" + + assert result["summary"] == expected + assert result["summary_html"] == expected_html + end + + test "a create activity with a summary containing invalid HTML" do + {:ok, activity} = + CommonAPI.post(insert(:user), %{ + "spoiler_text" => "<span style=\"color: magenta; font-size: 32px;\">meow</span>", + "status" => "." + }) + + result = ActivityView.render("activity.json", activity: activity) + + expected = "meow" + + assert result["summary"] == expected + assert result["summary_html"] == expected + end + test "a create activity with a note" do user = insert(:user) other_user = insert(:user, %{nickname: "shp"}) @@ -23,7 +130,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityViewTest do result = ActivityView.render("activity.json", activity: activity) - convo_id = TwitterAPI.context_to_conversation_id(object.data["context"]) + convo_id = Utils.context_to_conversation_id(object.data["context"]) expected = %{ "activity_type" => "post", @@ -46,15 +153,21 @@ defmodule Pleroma.Web.TwitterAPI.ActivityViewTest do "possibly_sensitive" => false, "repeat_num" => 0, "repeated" => false, + "pinned" => false, "statusnet_conversation_id" => convo_id, + "summary" => "", + "summary_html" => "", "statusnet_html" => - "Hey <span><a href=\"#{other_user.ap_id}\">@<span>shp</span></a></span>!", + "Hey <span class=\"h-card\"><a data-user=\"#{other_user.id}\" class=\"u-url mention\" href=\"#{ + other_user.ap_id + }\">@<span>shp</span></a></span>!", "tags" => [], "text" => "Hey @shp!", "uri" => object.data["id"], "user" => UserView.render("show.json", %{user: user}), "visibility" => "direct", - "summary" => nil + "card" => nil, + "muted" => false } assert result == expected @@ -66,12 +179,12 @@ defmodule Pleroma.Web.TwitterAPI.ActivityViewTest do {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!"}) object = Object.normalize(activity.data["object"]) - convo_id = TwitterAPI.context_to_conversation_id(object.data["context"]) + convo_id = Utils.context_to_conversation_id(object.data["context"]) mocks = [ { - TwitterAPI, - [], + Utils, + [:passthrough], [context_to_conversation_id: fn _ -> false end] }, { @@ -86,7 +199,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityViewTest do assert result["statusnet_conversation_id"] == convo_id assert result["user"] - refute called(TwitterAPI.context_to_conversation_id(:_)) + refute called(Utils.context_to_conversation_id(:_)) refute called(User.get_cached_by_ap_id(user.ap_id)) refute called(User.get_cached_by_ap_id(other_user.ap_id)) end @@ -114,6 +227,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityViewTest do {:ok, like, _object} = CommonAPI.favorite(activity.id, other_user) result = ActivityView.render("activity.json", activity: like) + activity = Pleroma.Activity.get_by_ap_id(activity.data["id"]) expected = %{ "activity_type" => "like", @@ -123,6 +237,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityViewTest do "in_reply_to_status_id" => activity.id, "is_local" => true, "is_post_verb" => false, + "favorited_status" => ActivityView.render("activity.json", activity: activity), "statusnet_html" => "shp favorited a status.", "text" => "shp favorited a status.", "uri" => "tag:#{like.data["id"]}:objectType=Favourite", @@ -150,6 +265,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityViewTest do "in_reply_to_status_id" => nil, "is_local" => true, "is_post_verb" => false, + "favorited_status" => nil, "statusnet_html" => "shp favorited a status.", "text" => "shp favorited a status.", "uri" => "tag:#{like.data["id"]}:objectType=Favourite", @@ -166,9 +282,9 @@ defmodule Pleroma.Web.TwitterAPI.ActivityViewTest do {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!"}) {:ok, announce, object} = CommonAPI.repeat(activity.id, other_user) - convo_id = TwitterAPI.context_to_conversation_id(object.data["context"]) + convo_id = Utils.context_to_conversation_id(object.data["context"]) - activity = Repo.get(Activity, activity.id) + activity = Activity.get_by_id(activity.id) result = ActivityView.render("activity.json", activity: announce) @@ -241,4 +357,18 @@ defmodule Pleroma.Web.TwitterAPI.ActivityViewTest do assert result == expected end + + test "a peertube video" do + {:ok, object} = + ActivityPub.fetch_object_from_id( + "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3" + ) + + %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"]) + + result = ActivityView.render("activity.json", activity: activity) + + assert length(result["attachments"]) == 1 + assert result["summary"] == "Friday Night" + end end diff --git a/test/web/twitter_api/views/notification_view_test.exs b/test/web/twitter_api/views/notification_view_test.exs index 79eafda7d..6baeeaf63 100644 --- a/test/web/twitter_api/views/notification_view_test.exs +++ b/test/web/twitter_api/views/notification_view_test.exs @@ -1,14 +1,18 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.TwitterAPI.NotificationViewTest do use Pleroma.DataCase - alias Pleroma.{User, Notification} - alias Pleroma.Web.TwitterAPI.TwitterAPI + alias Pleroma.Notification + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.CommonAPI.Utils + alias Pleroma.Web.TwitterAPI.ActivityView alias Pleroma.Web.TwitterAPI.NotificationView + alias Pleroma.Web.TwitterAPI.TwitterAPI alias Pleroma.Web.TwitterAPI.UserView - alias Pleroma.Web.TwitterAPI.ActivityView - alias Pleroma.Web.CommonAPI.Utils - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Builders.UserBuilder import Pleroma.Factory @@ -67,7 +71,7 @@ defmodule Pleroma.Web.TwitterAPI.NotificationViewTest do user = User.get_cached_by_ap_id(note_activity.data["actor"]) repeater = insert(:user) - {:ok, activity} = TwitterAPI.repeat(repeater, note_activity.id) + {:ok, _activity} = TwitterAPI.repeat(repeater, note_activity.id) [notification] = Notification.for_user(user) represented = %{ @@ -89,7 +93,7 @@ defmodule Pleroma.Web.TwitterAPI.NotificationViewTest do user = User.get_cached_by_ap_id(note_activity.data["actor"]) liker = insert(:user) - {:ok, activity} = TwitterAPI.fav(liker, note_activity.id) + {:ok, _activity} = TwitterAPI.fav(liker, note_activity.id) [notification] = Notification.for_user(user) represented = %{ diff --git a/test/web/twitter_api/views/user_view_test.exs b/test/web/twitter_api/views/user_view_test.exs index e69ca24a9..0feaf4b64 100644 --- a/test/web/twitter_api/views/user_view_test.exs +++ b/test/web/twitter_api/views/user_view_test.exs @@ -1,10 +1,13 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.TwitterAPI.UserViewTest do use Pleroma.DataCase alias Pleroma.User - alias Pleroma.Web.TwitterAPI.UserView alias Pleroma.Web.CommonAPI.Utils - alias Pleroma.Builders.UserBuilder + alias Pleroma.Web.TwitterAPI.UserView import Pleroma.Factory @@ -27,7 +30,7 @@ defmodule Pleroma.Web.TwitterAPI.UserViewTest do assert represented["profile_image_url"] == image end - test "A user with emoji in username", %{user: user} do + test "A user with emoji in username" do expected = "<img height=\"32px\" width=\"32px\" alt=\"karjalanpiirakka\" title=\"karjalanpiirakka\" src=\"/file.png\" /> man" @@ -87,7 +90,8 @@ defmodule Pleroma.Web.TwitterAPI.UserViewTest do "follows_you" => false, "statusnet_blocking" => false, "rights" => %{ - "delete_others_notice" => false + "delete_others_notice" => false, + "admin" => false }, "statusnet_profile_url" => user.ap_id, "cover_photo" => banner, @@ -96,7 +100,13 @@ defmodule Pleroma.Web.TwitterAPI.UserViewTest do "locked" => false, "default_scope" => "public", "no_rich_text" => false, - "fields" => [] + "hide_follows" => false, + "hide_followers" => false, + "fields" => [], + "pleroma" => %{ + "confirmation_pending" => false, + "tags" => [] + } } assert represented == UserView.render("show.json", %{user: user}) @@ -128,7 +138,8 @@ defmodule Pleroma.Web.TwitterAPI.UserViewTest do "follows_you" => false, "statusnet_blocking" => false, "rights" => %{ - "delete_others_notice" => false + "delete_others_notice" => false, + "admin" => false }, "statusnet_profile_url" => user.ap_id, "cover_photo" => banner, @@ -137,7 +148,13 @@ defmodule Pleroma.Web.TwitterAPI.UserViewTest do "locked" => false, "default_scope" => "public", "no_rich_text" => false, - "fields" => [] + "hide_follows" => false, + "hide_followers" => false, + "fields" => [], + "pleroma" => %{ + "confirmation_pending" => false, + "tags" => [] + } } assert represented == UserView.render("show.json", %{user: user, for: follower}) @@ -170,7 +187,8 @@ defmodule Pleroma.Web.TwitterAPI.UserViewTest do "follows_you" => true, "statusnet_blocking" => false, "rights" => %{ - "delete_others_notice" => false + "delete_others_notice" => false, + "admin" => false }, "statusnet_profile_url" => follower.ap_id, "cover_photo" => banner, @@ -179,7 +197,13 @@ defmodule Pleroma.Web.TwitterAPI.UserViewTest do "locked" => false, "default_scope" => "public", "no_rich_text" => false, - "fields" => [] + "hide_follows" => false, + "hide_followers" => false, + "fields" => [], + "pleroma" => %{ + "confirmation_pending" => false, + "tags" => [] + } } assert represented == UserView.render("show.json", %{user: follower, for: user}) @@ -190,6 +214,36 @@ defmodule Pleroma.Web.TwitterAPI.UserViewTest do represented = UserView.render("show.json", %{user: user, for: user}) assert represented["rights"]["delete_others_notice"] + assert represented["role"] == "moderator" + end + + test "a user that is a admin" do + user = insert(:user, %{info: %{is_admin: true}}) + represented = UserView.render("show.json", %{user: user, for: user}) + + assert represented["rights"]["admin"] + assert represented["role"] == "admin" + end + + test "A moderator with hidden role for another user", %{user: user} do + admin = insert(:user, %{info: %{is_moderator: true, show_role: false}}) + represented = UserView.render("show.json", %{user: admin, for: user}) + + assert represented["role"] == nil + end + + test "An admin with hidden role for another user", %{user: user} do + admin = insert(:user, %{info: %{is_admin: true, show_role: false}}) + represented = UserView.render("show.json", %{user: admin, for: user}) + + assert represented["role"] == nil + end + + test "A regular user for the admin", %{user: user} do + admin = insert(:user, %{info: %{is_admin: true}}) + represented = UserView.render("show.json", %{user: user, for: admin}) + + assert represented["pleroma"]["deactivated"] == false end test "A blocked user for the blocker" do @@ -219,7 +273,8 @@ defmodule Pleroma.Web.TwitterAPI.UserViewTest do "follows_you" => false, "statusnet_blocking" => true, "rights" => %{ - "delete_others_notice" => false + "delete_others_notice" => false, + "admin" => false }, "statusnet_profile_url" => user.ap_id, "cover_photo" => banner, @@ -228,10 +283,16 @@ defmodule Pleroma.Web.TwitterAPI.UserViewTest do "locked" => false, "default_scope" => "public", "no_rich_text" => false, - "fields" => [] + "hide_follows" => false, + "hide_followers" => false, + "fields" => [], + "pleroma" => %{ + "confirmation_pending" => false, + "tags" => [] + } } - blocker = Repo.get(User, blocker.id) + blocker = User.get_by_id(blocker.id) assert represented == UserView.render("show.json", %{user: user, for: blocker}) end diff --git a/test/web/views/error_view_test.exs b/test/web/views/error_view_test.exs index 1d443b187..d529fd2c3 100644 --- a/test/web/views/error_view_test.exs +++ b/test/web/views/error_view_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.ErrorViewTest do use Pleroma.Web.ConnCase, async: true @@ -10,11 +14,16 @@ defmodule Pleroma.Web.ErrorViewTest do test "render 500.json" do assert render(Pleroma.Web.ErrorView, "500.json", []) == - %{errors: %{detail: "Internal server error"}} + %{errors: %{detail: "Internal server error", reason: "nil"}} end test "render any other" do assert render(Pleroma.Web.ErrorView, "505.json", []) == - %{errors: %{detail: "Internal server error"}} + %{errors: %{detail: "Internal server error", reason: "nil"}} + end + + test "render 500.json with reason" do + assert render(Pleroma.Web.ErrorView, "500.json", reason: "test reason") == + %{errors: %{detail: "Internal server error", reason: "\"test reason\""}} end end diff --git a/test/web/web_finger/web_finger_controller_test.exs b/test/web/web_finger/web_finger_controller_test.exs new file mode 100644 index 000000000..43fccfc7a --- /dev/null +++ b/test/web/web_finger/web_finger_controller_test.exs @@ -0,0 +1,46 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + import Tesla.Mock + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + test "Webfinger JRD" do + user = insert(:user) + + response = + build_conn() + |> put_req_header("accept", "application/jrd+json") + |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost") + + assert json_response(response, 200)["subject"] == "acct:#{user.nickname}@localhost" + end + + test "Webfinger XML" do + user = insert(:user) + + response = + build_conn() + |> put_req_header("accept", "application/xrd+xml") + |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost") + + assert response(response, 200) + end + + test "Sends a 400 when resource param is missing" do + response = + build_conn() + |> put_req_header("accept", "application/xrd+xml,application/jrd+json") + |> get("/.well-known/webfinger") + + assert response(response, 400) + end +end diff --git a/test/web/web_finger/web_finger_test.exs b/test/web/web_finger/web_finger_test.exs index 28d429565..6b20d8d56 100644 --- a/test/web/web_finger/web_finger_test.exs +++ b/test/web/web_finger/web_finger_test.exs @@ -1,7 +1,17 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.WebFingerTest do use Pleroma.DataCase alias Pleroma.Web.WebFinger import Pleroma.Factory + import Tesla.Mock + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end describe "host meta" do test "returns a link to the xml lrdd" do diff --git a/test/web/websub/websub_controller_test.exs b/test/web/websub/websub_controller_test.exs index d861c241f..1e69ed01a 100644 --- a/test/web/websub/websub_controller_test.exs +++ b/test/web/websub/websub_controller_test.exs @@ -1,9 +1,14 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.Websub.WebsubControllerTest do use Pleroma.Web.ConnCase import Pleroma.Factory - alias Pleroma.Web.Websub.WebsubClientSubscription - alias Pleroma.{Repo, Activity} + alias Pleroma.Activity + alias Pleroma.Repo alias Pleroma.Web.Websub + alias Pleroma.Web.Websub.WebsubClientSubscription test "websub subscription request", %{conn: conn} do user = insert(:user) @@ -46,35 +51,37 @@ defmodule Pleroma.Web.Websub.WebsubControllerTest do assert_in_delta NaiveDateTime.diff(websub.valid_until, NaiveDateTime.utc_now()), 100, 5 end - test "handles incoming feed updates", %{conn: conn} do - websub = insert(:websub_client_subscription) - doc = "some stuff" - signature = Websub.sign(websub.secret, doc) + describe "websub_incoming" do + test "handles incoming feed updates", %{conn: conn} do + websub = insert(:websub_client_subscription) + doc = "some stuff" + signature = Websub.sign(websub.secret, doc) - conn = - conn - |> put_req_header("x-hub-signature", "sha1=" <> signature) - |> put_req_header("content-type", "application/atom+xml") - |> post("/push/subscriptions/#{websub.id}", doc) + conn = + conn + |> put_req_header("x-hub-signature", "sha1=" <> signature) + |> put_req_header("content-type", "application/atom+xml") + |> post("/push/subscriptions/#{websub.id}", doc) - assert response(conn, 200) == "OK" + assert response(conn, 200) == "OK" - assert length(Repo.all(Activity)) == 1 - end + assert length(Repo.all(Activity)) == 1 + end - test "rejects incoming feed updates with the wrong signature", %{conn: conn} do - websub = insert(:websub_client_subscription) - doc = "some stuff" - signature = Websub.sign("wrong secret", doc) + test "rejects incoming feed updates with the wrong signature", %{conn: conn} do + websub = insert(:websub_client_subscription) + doc = "some stuff" + signature = Websub.sign("wrong secret", doc) - conn = - conn - |> put_req_header("x-hub-signature", "sha1=" <> signature) - |> put_req_header("content-type", "application/atom+xml") - |> post("/push/subscriptions/#{websub.id}", doc) + conn = + conn + |> put_req_header("x-hub-signature", "sha1=" <> signature) + |> put_req_header("content-type", "application/atom+xml") + |> post("/push/subscriptions/#{websub.id}", doc) - assert response(conn, 500) == "Error" + assert response(conn, 500) == "Error" - assert length(Repo.all(Activity)) == 0 + assert Enum.empty?(Repo.all(Activity)) + end end end diff --git a/test/web/websub/websub_test.exs b/test/web/websub/websub_test.exs index da7bc9112..74386d7db 100644 --- a/test/web/websub/websub_test.exs +++ b/test/web/websub/websub_test.exs @@ -1,15 +1,22 @@ -defmodule Pleroma.Web.WebsubMock do - def verify(sub) do - {:ok, sub} - end -end +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.WebsubTest do use Pleroma.DataCase + + alias Pleroma.Web.Router.Helpers alias Pleroma.Web.Websub - alias Pleroma.Web.Websub.{WebsubServerSubscription, WebsubClientSubscription} + alias Pleroma.Web.Websub.WebsubClientSubscription + alias Pleroma.Web.Websub.WebsubServerSubscription + import Pleroma.Factory - alias Pleroma.Web.Router.Helpers + import Tesla.Mock + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end test "a verification of a request that is accepted" do sub = insert(:websub_subscription) @@ -26,8 +33,8 @@ defmodule Pleroma.Web.WebsubTest do assert String.to_integer(seconds) > 0 {:ok, - %HTTPoison.Response{ - status_code: 200, + %Tesla.Env{ + status: 200, body: challenge }} end @@ -41,8 +48,8 @@ defmodule Pleroma.Web.WebsubTest do getter = fn _path, _headers, _options -> {:ok, - %HTTPoison.Response{ - status_code: 500, + %Tesla.Env{ + status: 500, body: "" }} end @@ -113,12 +120,7 @@ defmodule Pleroma.Web.WebsubTest do test "discovers the hub and canonical url" do topic = "https://mastodon.social/users/lambadalambda.atom" - getter = fn ^topic -> - doc = File.read!("test/fixtures/lambadalambda.atom") - {:ok, %{status_code: 200, body: doc}} - end - - {:ok, discovered} = Websub.gather_feed_data(topic, getter) + {:ok, discovered} = Websub.gather_feed_data(topic) expected = %{ "hub" => "https://mastodon.social/api/push", @@ -158,7 +160,7 @@ defmodule Pleroma.Web.WebsubTest do websub.id ) - {:ok, %{status_code: 202}} + {:ok, %{status: 202}} end task = Task.async(fn -> Websub.request_subscription(websub, poster) end) @@ -177,7 +179,7 @@ defmodule Pleroma.Web.WebsubTest do websub = insert(:websub_client_subscription, %{hub: hub, topic: topic}) poster = fn ^hub, {:form, _data}, _headers -> - {:ok, %{status_code: 202}} + {:ok, %{status: 202}} end {:error, websub} = Websub.request_subscription(websub, poster, 1000) @@ -186,7 +188,7 @@ defmodule Pleroma.Web.WebsubTest do websub = insert(:websub_client_subscription, %{hub: hub, topic: topic}) poster = fn ^hub, {:form, _data}, _headers -> - {:ok, %{status_code: 400}} + {:ok, %{status: 400}} end {:error, websub} = Websub.request_subscription(websub, poster, 1000) @@ -209,6 +211,7 @@ defmodule Pleroma.Web.WebsubTest do insert(:websub_client_subscription, %{ valid_until: NaiveDateTime.add(now, 2 * day), topic: "http://example.org/still_good", + hub: "http://example.org/still_good", state: "accepted" }) @@ -216,6 +219,7 @@ defmodule Pleroma.Web.WebsubTest do insert(:websub_client_subscription, %{ valid_until: NaiveDateTime.add(now, day - 100), topic: "http://example.org/needs_refresh", + hub: "http://example.org/needs_refresh", state: "accepted" }) diff --git a/test/xml_builder_test.exs b/test/xml_builder_test.exs index 4be7bbd01..a7742f339 100644 --- a/test/xml_builder_test.exs +++ b/test/xml_builder_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.XmlBuilderTest do use Pleroma.DataCase alias Pleroma.XmlBuilder |