Fix broken tests
This commit is contained in:
parent
4a3dd5f65e
commit
ddbe989461
6 changed files with 196 additions and 144 deletions
|
@ -176,7 +176,7 @@ defmodule Pleroma.HTMLTest do
|
||||||
})
|
})
|
||||||
|
|
||||||
object = Object.normalize(activity, fetch: false)
|
object = Object.normalize(activity, fetch: false)
|
||||||
{:ok, url} = HTML.extract_first_external_url_from_object(object)
|
url = HTML.extract_first_external_url_from_object(object)
|
||||||
assert url == "https://github.com/komeiji-satori/Dress"
|
assert url == "https://github.com/komeiji-satori/Dress"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -191,7 +191,7 @@ defmodule Pleroma.HTMLTest do
|
||||||
})
|
})
|
||||||
|
|
||||||
object = Object.normalize(activity, fetch: false)
|
object = Object.normalize(activity, fetch: false)
|
||||||
{:ok, url} = HTML.extract_first_external_url_from_object(object)
|
url = HTML.extract_first_external_url_from_object(object)
|
||||||
|
|
||||||
assert url == "https://github.com/syuilo/misskey/blob/develop/docs/setup.en.md"
|
assert url == "https://github.com/syuilo/misskey/blob/develop/docs/setup.en.md"
|
||||||
|
|
||||||
|
@ -207,7 +207,7 @@ defmodule Pleroma.HTMLTest do
|
||||||
})
|
})
|
||||||
|
|
||||||
object = Object.normalize(activity, fetch: false)
|
object = Object.normalize(activity, fetch: false)
|
||||||
{:ok, url} = HTML.extract_first_external_url_from_object(object)
|
url = HTML.extract_first_external_url_from_object(object)
|
||||||
|
|
||||||
assert url == "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=72255140"
|
assert url == "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=72255140"
|
||||||
end
|
end
|
||||||
|
@ -223,7 +223,7 @@ defmodule Pleroma.HTMLTest do
|
||||||
})
|
})
|
||||||
|
|
||||||
object = Object.normalize(activity, fetch: false)
|
object = Object.normalize(activity, fetch: false)
|
||||||
{:ok, url} = HTML.extract_first_external_url_from_object(object)
|
url = HTML.extract_first_external_url_from_object(object)
|
||||||
|
|
||||||
assert url == "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=72255140"
|
assert url == "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=72255140"
|
||||||
end
|
end
|
||||||
|
@ -235,7 +235,7 @@ defmodule Pleroma.HTMLTest do
|
||||||
|
|
||||||
object = Object.normalize(activity, fetch: false)
|
object = Object.normalize(activity, fetch: false)
|
||||||
|
|
||||||
assert {:ok, nil} = HTML.extract_first_external_url_from_object(object)
|
assert nil == HTML.extract_first_external_url_from_object(object)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "skips attachment links" do
|
test "skips attachment links" do
|
||||||
|
@ -249,7 +249,7 @@ defmodule Pleroma.HTMLTest do
|
||||||
|
|
||||||
object = Object.normalize(activity, fetch: false)
|
object = Object.normalize(activity, fetch: false)
|
||||||
|
|
||||||
assert {:ok, nil} = HTML.extract_first_external_url_from_object(object)
|
assert nil == HTML.extract_first_external_url_from_object(object)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,6 +16,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
|
||||||
alias Pleroma.Web.CommonAPI
|
alias Pleroma.Web.CommonAPI
|
||||||
alias Pleroma.Web.MastodonAPI.AccountView
|
alias Pleroma.Web.MastodonAPI.AccountView
|
||||||
alias Pleroma.Web.MastodonAPI.StatusView
|
alias Pleroma.Web.MastodonAPI.StatusView
|
||||||
|
alias Pleroma.Web.RichMedia.Card
|
||||||
|
|
||||||
import Pleroma.Factory
|
import Pleroma.Factory
|
||||||
import Tesla.Mock
|
import Tesla.Mock
|
||||||
|
@ -677,56 +678,88 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
|
||||||
|
|
||||||
describe "rich media cards" do
|
describe "rich media cards" do
|
||||||
test "a rich media card without a site name renders correctly" do
|
test "a rich media card without a site name renders correctly" do
|
||||||
page_url = "http://example.com"
|
page_url = "https://example.com"
|
||||||
|
|
||||||
card = %{
|
{:ok, card} =
|
||||||
url: page_url,
|
Card.create(page_url, %{image: page_url <> "/example.jpg", title: "Example website"})
|
||||||
image: page_url <> "/example.jpg",
|
|
||||||
title: "Example website"
|
|
||||||
}
|
|
||||||
|
|
||||||
%{provider_name: "example.com"} =
|
%{provider_name: "example.com"} = StatusView.render("card.json", card)
|
||||||
StatusView.render("card.json", %{page_url: page_url, rich_media: card})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "a rich media card without a site name or image renders correctly" do
|
test "a rich media card without a site name or image renders correctly" do
|
||||||
page_url = "http://example.com"
|
page_url = "https://example.com"
|
||||||
|
|
||||||
card = %{
|
fields = %{
|
||||||
url: page_url,
|
"url" => page_url,
|
||||||
title: "Example website"
|
"title" => "Example website"
|
||||||
}
|
}
|
||||||
|
|
||||||
%{provider_name: "example.com"} =
|
{:ok, card} = Card.create(page_url, fields)
|
||||||
StatusView.render("card.json", %{page_url: page_url, rich_media: card})
|
|
||||||
|
%{provider_name: "example.com"} = StatusView.render("card.json", card)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "a rich media card without an image renders correctly" do
|
test "a rich media card without an image renders correctly" do
|
||||||
page_url = "http://example.com"
|
page_url = "https://example.com"
|
||||||
|
|
||||||
card = %{
|
fields = %{
|
||||||
url: page_url,
|
"url" => page_url,
|
||||||
site_name: "Example site name",
|
"site_name" => "Example site name",
|
||||||
title: "Example website"
|
"title" => "Example website"
|
||||||
}
|
}
|
||||||
|
|
||||||
%{provider_name: "example.com"} =
|
{:ok, card} = Card.create(page_url, fields)
|
||||||
StatusView.render("card.json", %{page_url: page_url, rich_media: card})
|
|
||||||
|
%{provider_name: "example.com"} = StatusView.render("card.json", card)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "a rich media card with all relevant data renders correctly" do
|
test "a rich media card with all relevant data renders correctly" do
|
||||||
page_url = "http://example.com"
|
page_url = "https://example.com"
|
||||||
|
|
||||||
card = %{
|
fields = %{
|
||||||
url: page_url,
|
"url" => page_url,
|
||||||
site_name: "Example site name",
|
"site_name" => "Example site name",
|
||||||
title: "Example website",
|
"title" => "Example website",
|
||||||
image: page_url <> "/example.jpg",
|
"image" => page_url <> "/example.jpg",
|
||||||
description: "Example description"
|
"description" => "Example description"
|
||||||
}
|
}
|
||||||
|
|
||||||
%{provider_name: "example.com"} =
|
{:ok, card} = Card.create(page_url, fields)
|
||||||
StatusView.render("card.json", %{page_url: page_url, rich_media: card})
|
|
||||||
|
%{provider_name: "example.com"} = StatusView.render("card.json", card)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "a rich media card has all media proxied" do
|
||||||
|
clear_config([:media_proxy, :enabled], true)
|
||||||
|
clear_config([:media_preview_proxy, :enabled])
|
||||||
|
|
||||||
|
ConfigMock
|
||||||
|
|> stub_with(Pleroma.Test.StaticConfig)
|
||||||
|
|
||||||
|
page_url = "https://example.com"
|
||||||
|
|
||||||
|
fields = %{
|
||||||
|
"url" => page_url,
|
||||||
|
"site_name" => "Example site name",
|
||||||
|
"title" => "Example website",
|
||||||
|
"image" => page_url <> "/example.jpg",
|
||||||
|
"audio" => page_url <> "/example.ogg",
|
||||||
|
"video" => page_url <> "/example.mp4",
|
||||||
|
"description" => "Example description"
|
||||||
|
}
|
||||||
|
|
||||||
|
{:ok, card} = Card.create(page_url, fields)
|
||||||
|
|
||||||
|
%{
|
||||||
|
provider_name: "example.com",
|
||||||
|
image: image,
|
||||||
|
pleroma: %{opengraph: og}
|
||||||
|
} = StatusView.render("card.json", card)
|
||||||
|
|
||||||
|
assert String.match?(image, ~r/\/proxy\//)
|
||||||
|
assert String.match?(og["image"], ~r/\/proxy\//)
|
||||||
|
assert String.match?(og["audio"], ~r/\/proxy\//)
|
||||||
|
assert String.match?(og["video"], ~r/\/proxy\//)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
71
test/pleroma/web/rich_media/card_test.exs
Normal file
71
test/pleroma/web/rich_media/card_test.exs
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.RichMedia.CardTest do
|
||||||
|
use Pleroma.DataCase, async: true
|
||||||
|
|
||||||
|
alias Pleroma.UnstubbedConfigMock, as: ConfigMock
|
||||||
|
alias Pleroma.Web.CommonAPI
|
||||||
|
alias Pleroma.Web.RichMedia.Card
|
||||||
|
|
||||||
|
import Mox
|
||||||
|
import Pleroma.Factory
|
||||||
|
import Tesla.Mock
|
||||||
|
|
||||||
|
setup do
|
||||||
|
mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
|
||||||
|
|
||||||
|
ConfigMock
|
||||||
|
|> stub_with(Pleroma.Test.StaticConfig)
|
||||||
|
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
setup do: clear_config([:rich_media, :enabled], true)
|
||||||
|
|
||||||
|
test "crawls URL in activity" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
url = "https://example.com/ogp"
|
||||||
|
url_hash = Card.url_to_hash(url)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(user, %{
|
||||||
|
status: "[test](#{url})",
|
||||||
|
content_type: "text/markdown"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert %Card{url_hash: ^url_hash, fields: _} = Card.get_by_activity(activity)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "recrawls URLs on updates" do
|
||||||
|
original_url = "https://google.com/"
|
||||||
|
original_url_hash = Card.url_to_hash(original_url)
|
||||||
|
updated_url = "https://yahoo.com/"
|
||||||
|
updated_url_hash = Card.url_to_hash(updated_url)
|
||||||
|
|
||||||
|
user = insert(:user)
|
||||||
|
{:ok, activity} = CommonAPI.post(user, %{status: "I like this site #{original_url}"})
|
||||||
|
|
||||||
|
# Force a backfill
|
||||||
|
Card.get_by_activity(activity)
|
||||||
|
|
||||||
|
assert match?(
|
||||||
|
%Card{url_hash: ^original_url_hash, fields: _},
|
||||||
|
Card.get_by_activity(activity)
|
||||||
|
)
|
||||||
|
|
||||||
|
{:ok, _} = CommonAPI.update(user, activity, %{status: "I like this site #{updated_url}"})
|
||||||
|
|
||||||
|
activity = Pleroma.Activity.get_by_id(activity.id)
|
||||||
|
|
||||||
|
# Force a backfill
|
||||||
|
Card.get_by_activity(activity)
|
||||||
|
|
||||||
|
assert match?(
|
||||||
|
%Card{url_hash: ^updated_url_hash, fields: _},
|
||||||
|
Card.get_by_activity(activity)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,91 +0,0 @@
|
||||||
# Pleroma: A lightweight social networking server
|
|
||||||
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
defmodule Pleroma.Web.RichMedia.HelpersTest do
|
|
||||||
use Pleroma.DataCase
|
|
||||||
|
|
||||||
alias Pleroma.Web.CommonAPI
|
|
||||||
alias Pleroma.Web.RichMedia.Helpers
|
|
||||||
|
|
||||||
import Pleroma.Factory
|
|
||||||
import Tesla.Mock
|
|
||||||
|
|
||||||
setup do
|
|
||||||
mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
|
|
||||||
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
|
|
||||||
setup do: clear_config([:rich_media, :enabled])
|
|
||||||
|
|
||||||
test "refuses to crawl incomplete URLs" do
|
|
||||||
user = insert(:user)
|
|
||||||
|
|
||||||
{:ok, activity} =
|
|
||||||
CommonAPI.post(user, %{
|
|
||||||
status: "[test](example.com/ogp)",
|
|
||||||
content_type: "text/markdown"
|
|
||||||
})
|
|
||||||
|
|
||||||
clear_config([:rich_media, :enabled], true)
|
|
||||||
|
|
||||||
assert %{} == Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
|
|
||||||
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"
|
|
||||||
})
|
|
||||||
|
|
||||||
clear_config([:rich_media, :enabled], true)
|
|
||||||
|
|
||||||
assert %{} == Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "crawls valid, complete URLs" do
|
|
||||||
user = insert(:user)
|
|
||||||
|
|
||||||
{:ok, activity} =
|
|
||||||
CommonAPI.post(user, %{
|
|
||||||
status: "[test](https://example.com/ogp)",
|
|
||||||
content_type: "text/markdown"
|
|
||||||
})
|
|
||||||
|
|
||||||
clear_config([:rich_media, :enabled], true)
|
|
||||||
|
|
||||||
assert %{page_url: "https://example.com/ogp", rich_media: _} =
|
|
||||||
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "refuses to crawl URLs of private network from posts" do
|
|
||||||
user = insert(:user)
|
|
||||||
|
|
||||||
{:ok, activity} =
|
|
||||||
CommonAPI.post(user, %{status: "http://127.0.0.1:4000/notice/9kCP7VNyPJXFOXDrgO"})
|
|
||||||
|
|
||||||
{:ok, activity2} = CommonAPI.post(user, %{status: "https://10.111.10.1/notice/9kCP7V"})
|
|
||||||
{:ok, activity3} = CommonAPI.post(user, %{status: "https://172.16.32.40/notice/9kCP7V"})
|
|
||||||
{:ok, activity4} = CommonAPI.post(user, %{status: "https://192.168.10.40/notice/9kCP7V"})
|
|
||||||
{:ok, activity5} = CommonAPI.post(user, %{status: "https://pleroma.local/notice/9kCP7V"})
|
|
||||||
|
|
||||||
clear_config([:rich_media, :enabled], true)
|
|
||||||
|
|
||||||
assert %{} = Helpers.fetch_data_for_activity(activity)
|
|
||||||
assert %{} = Helpers.fetch_data_for_activity(activity2)
|
|
||||||
assert %{} = Helpers.fetch_data_for_activity(activity3)
|
|
||||||
assert %{} = Helpers.fetch_data_for_activity(activity4)
|
|
||||||
assert %{} = Helpers.fetch_data_for_activity(activity5)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "catches errors in fetching" do
|
|
||||||
Tesla.Mock.mock(fn _ -> raise ArgumentError end)
|
|
||||||
|
|
||||||
assert {:error, :fetch_error} ==
|
|
||||||
Helpers.rich_media_get("wp-json/oembed/1.0/embed?url=http:%252F%252F")
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -3,8 +3,22 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrlTest do
|
defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrlTest do
|
||||||
# Relies on Cachex, needs to be synchronous
|
use Pleroma.DataCase, async: false
|
||||||
use Pleroma.DataCase
|
use Oban.Testing, repo: Pleroma.Repo
|
||||||
|
|
||||||
|
import Mox
|
||||||
|
|
||||||
|
alias Pleroma.UnstubbedConfigMock, as: ConfigMock
|
||||||
|
alias Pleroma.Web.RichMedia.Card
|
||||||
|
|
||||||
|
setup do
|
||||||
|
ConfigMock
|
||||||
|
|> stub_with(Pleroma.Test.StaticConfig)
|
||||||
|
|
||||||
|
clear_config([:rich_media, :enabled], true)
|
||||||
|
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
test "s3 signed url is parsed correct for expiration time" do
|
test "s3 signed url is parsed correct for expiration time" do
|
||||||
url = "https://pleroma.social/amz"
|
url = "https://pleroma.social/amz"
|
||||||
|
@ -43,26 +57,29 @@ defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrlTest do
|
||||||
<meta name="twitter:site" content="Pleroma" />
|
<meta name="twitter:site" content="Pleroma" />
|
||||||
<meta name="twitter:title" content="Pleroma" />
|
<meta name="twitter:title" content="Pleroma" />
|
||||||
<meta name="twitter:description" content="Pleroma" />
|
<meta name="twitter:description" content="Pleroma" />
|
||||||
<meta name="twitter:image" content="#{Map.get(metadata, :image)}" />
|
<meta name="twitter:image" content="#{Map.get(metadata, "image")}" />
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Tesla.Mock.mock(fn
|
Tesla.Mock.mock(fn
|
||||||
%{
|
%{
|
||||||
method: :get,
|
method: :get,
|
||||||
url: "https://pleroma.social/amz"
|
url: ^url
|
||||||
} ->
|
} ->
|
||||||
%Tesla.Env{status: 200, body: body}
|
%Tesla.Env{status: 200, body: body}
|
||||||
|
|
||||||
|
%{method: :head} ->
|
||||||
|
%Tesla.Env{status: 200}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
Cachex.put(:rich_media_cache, url, metadata)
|
Card.get_or_backfill_by_url(url)
|
||||||
|
|
||||||
Pleroma.Web.RichMedia.Parser.set_ttl_based_on_image(metadata, url)
|
assert_enqueued(worker: Pleroma.Workers.RichMediaExpirationWorker, args: %{"url" => url})
|
||||||
|
|
||||||
{:ok, cache_ttl} = Cachex.ttl(:rich_media_cache, url)
|
[%Oban.Job{scheduled_at: scheduled_at}] = all_enqueued()
|
||||||
|
|
||||||
# as there is delay in setting and pulling the data from cache we ignore 1 second
|
timestamp_dt = Timex.parse!(timestamp, "{ISO:Basic:Z}")
|
||||||
# make it 2 seconds for flakyness
|
|
||||||
assert_in_delta(valid_till * 1000, cache_ttl, 2000)
|
assert DateTime.diff(scheduled_at, timestamp_dt) == valid_till
|
||||||
end
|
end
|
||||||
|
|
||||||
defp construct_s3_url(timestamp, valid_till) do
|
defp construct_s3_url(timestamp, valid_till) do
|
||||||
|
@ -71,11 +88,11 @@ defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrlTest do
|
||||||
|
|
||||||
defp construct_metadata(timestamp, valid_till, url) do
|
defp construct_metadata(timestamp, valid_till, url) do
|
||||||
%{
|
%{
|
||||||
image: construct_s3_url(timestamp, valid_till),
|
"image" => construct_s3_url(timestamp, valid_till),
|
||||||
site: "Pleroma",
|
"site" => "Pleroma",
|
||||||
title: "Pleroma",
|
"title" => "Pleroma",
|
||||||
description: "Pleroma",
|
"description" => "Pleroma",
|
||||||
url: url
|
"url" => url
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,8 +3,7 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
defmodule Pleroma.Web.RichMedia.ParserTest do
|
defmodule Pleroma.Web.RichMedia.ParserTest do
|
||||||
use ExUnit.Case
|
use Pleroma.DataCase
|
||||||
|
|
||||||
alias Pleroma.Web.RichMedia.Parser
|
alias Pleroma.Web.RichMedia.Parser
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
|
@ -173,4 +172,27 @@ defmodule Pleroma.Web.RichMedia.ParserTest do
|
||||||
test "does a HEAD request to check if the body is html" do
|
test "does a HEAD request to check if the body is html" do
|
||||||
assert {:error, {:content_type, _}} = Parser.parse("http://example.com/pdf-file")
|
assert {:error, {:content_type, _}} = Parser.parse("http://example.com/pdf-file")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "refuses to crawl incomplete URLs" do
|
||||||
|
url = "example.com/ogp"
|
||||||
|
assert :error == Parser.parse(url)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "refuses to crawl malformed URLs" do
|
||||||
|
url = "example.com[]/ogp"
|
||||||
|
assert :error == Parser.parse(url)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "refuses to crawl URLs of private network from posts" do
|
||||||
|
[
|
||||||
|
"http://127.0.0.1:4000/notice/9kCP7VNyPJXFOXDrgO",
|
||||||
|
"https://10.111.10.1/notice/9kCP7V",
|
||||||
|
"https://172.16.32.40/notice/9kCP7V",
|
||||||
|
"https://192.168.10.40/notice/9kCP7V",
|
||||||
|
"https://pleroma.local/notice/9kCP7V"
|
||||||
|
]
|
||||||
|
|> Enum.each(fn url ->
|
||||||
|
assert :error == Parser.parse(url)
|
||||||
|
end)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue