fix(media-proxy): safely detect extensionless images
This commit is contained in:
parent
465a81ecf2
commit
d5f48101b8
11 changed files with 211 additions and 609 deletions
|
|
@ -222,6 +222,56 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
assert Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"]
|
||||
end
|
||||
|
||||
test "sniffs and preserves an image body with a generic content type", %{conn: conn} do
|
||||
body = File.read!("test/fixtures/image.jpg")
|
||||
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, "/extensionless", headers, _, _ ->
|
||||
assert {"accept-encoding", "identity"} in headers
|
||||
{:ok, 200, [{"content-type", "application/octet-stream"}], %{body: body}}
|
||||
end)
|
||||
|> expect(:stream_body, fn %{body: ^body} = client ->
|
||||
{:ok, body, Map.delete(client, :body)}
|
||||
end)
|
||||
|> expect(:stream_body, fn %{} -> :done end)
|
||||
|
||||
conn =
|
||||
ReverseProxy.call(conn, "/extensionless",
|
||||
sniff_content_type: true,
|
||||
max_read_duration: :infinity,
|
||||
req_headers: [{"accept-encoding", "gzip"}]
|
||||
)
|
||||
|
||||
assert conn.resp_body == body
|
||||
assert Conn.get_resp_header(conn, "content-type") == ["image/jpeg"]
|
||||
|
||||
assert Conn.get_resp_header(conn, "content-disposition") == [
|
||||
"inline; filename=\"inline.jpg\""
|
||||
]
|
||||
end
|
||||
|
||||
test "preserves a generic content type for a non-image body", %{conn: conn} do
|
||||
body = "not an image"
|
||||
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, "/extensionless", _, _, _ ->
|
||||
{:ok, 200, [], %{body: body}}
|
||||
end)
|
||||
|> expect(:stream_body, fn %{body: ^body} = client ->
|
||||
{:ok, body, Map.delete(client, :body)}
|
||||
end)
|
||||
|> expect(:stream_body, fn %{} -> :done end)
|
||||
|
||||
conn = ReverseProxy.call(conn, "/extensionless", sniff_content_type: true)
|
||||
|
||||
assert conn.resp_body == body
|
||||
assert Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"]
|
||||
|
||||
assert Conn.get_resp_header(conn, "content-disposition") == [
|
||||
"attachment; filename=\"extensionless\""
|
||||
]
|
||||
end
|
||||
|
||||
defp headers_mock(_) do
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, "/headers", headers, _, _ ->
|
||||
|
|
@ -298,21 +348,21 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
end
|
||||
end
|
||||
|
||||
defp disposition_headers_mock(headers, url \\ "/disposition") do
|
||||
defp disposition_headers_mock(headers) do
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, ^url, _, _, _ ->
|
||||
Registry.register(ClientMock, url, 0)
|
||||
|> expect(:request, fn :get, "/disposition", _, _, _ ->
|
||||
Registry.register(ClientMock, "/disposition", 0)
|
||||
|
||||
{:ok, 200, headers, %{url: url}}
|
||||
{:ok, 200, headers, %{url: "/disposition"}}
|
||||
end)
|
||||
|> expect(:stream_body, 2, fn %{url: ^url} = client ->
|
||||
case Registry.lookup(ClientMock, url) do
|
||||
|> expect(:stream_body, 2, fn %{url: "/disposition"} = client ->
|
||||
case Registry.lookup(ClientMock, "/disposition") do
|
||||
[{_, 0}] ->
|
||||
Registry.update_value(ClientMock, url, &(&1 + 1))
|
||||
Registry.update_value(ClientMock, "/disposition", &(&1 + 1))
|
||||
{:ok, "", client}
|
||||
|
||||
[{_, 1}] ->
|
||||
Registry.unregister(ClientMock, url)
|
||||
Registry.unregister(ClientMock, "/disposition")
|
||||
:done
|
||||
end
|
||||
end)
|
||||
|
|
@ -337,6 +387,7 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
disposition_headers_mock([
|
||||
{"content-type", "image/png"},
|
||||
{"content-disposition", "attachment; filename=\"filename.png\""},
|
||||
{"content-disposition", "attachment; filename=\"duplicate.png\""},
|
||||
{"content-length", "0"}
|
||||
])
|
||||
|
||||
|
|
@ -347,23 +398,6 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
refute String.starts_with?(disposition, "attachment")
|
||||
end
|
||||
|
||||
test "forces inline based on content type even without a file extension in the url", %{
|
||||
conn: conn
|
||||
} do
|
||||
disposition_headers_mock(
|
||||
[
|
||||
{"content-type", "image/jpeg"},
|
||||
{"content-length", "0"}
|
||||
],
|
||||
"/original"
|
||||
)
|
||||
|
||||
conn = ReverseProxy.call(conn, "/original")
|
||||
|
||||
[disposition] = Conn.get_resp_header(conn, "content-disposition")
|
||||
assert disposition == "inline; filename=\"inline.jpg\""
|
||||
end
|
||||
|
||||
test "with content-disposition header", %{conn: conn} do
|
||||
disposition_headers_mock([
|
||||
{"content-disposition", "attachment; filename=\"filename.jpg\""},
|
||||
|
|
@ -390,69 +424,6 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
assert {"content-disposition", "attachment; filename=\"upstream.png\""} in conn.resp_headers
|
||||
end
|
||||
|
||||
test "with inline_content_types: true does not synthesise inline when upstream is absent", %{
|
||||
conn: conn
|
||||
} do
|
||||
disposition_headers_mock([
|
||||
{"content-type", "image/png"},
|
||||
{"content-length", "0"}
|
||||
])
|
||||
|
||||
conn = ReverseProxy.call(conn, "/disposition", inline_content_types: true)
|
||||
|
||||
assert Conn.get_resp_header(conn, "content-disposition") == []
|
||||
end
|
||||
|
||||
test "with inline_content_types: false forces attachment for everything", %{
|
||||
conn: conn
|
||||
} do
|
||||
disposition_headers_mock([
|
||||
{"content-type", "image/png"},
|
||||
{"content-disposition", "inline; filename=\"pic.png\""},
|
||||
{"content-length", "0"}
|
||||
])
|
||||
|
||||
conn = ReverseProxy.call(conn, "/disposition", inline_content_types: false)
|
||||
|
||||
[disposition] = Conn.get_resp_header(conn, "content-disposition")
|
||||
assert String.starts_with?(disposition, "attachment")
|
||||
end
|
||||
|
||||
test "with inline_content_types: false derives attachment filename from the URL basename when no upstream filename", %{
|
||||
conn: conn
|
||||
} do
|
||||
# No content-disposition header: the attachment branch falls back to
|
||||
# attachment_name, which by default comes from MediaProxy.filename/1
|
||||
# (the URL basename).
|
||||
disposition_headers_mock([
|
||||
{"content-type", "image/png"},
|
||||
{"content-length", "0"}
|
||||
])
|
||||
|
||||
conn = ReverseProxy.call(conn, "/disposition", inline_content_types: false)
|
||||
|
||||
[disposition] = Conn.get_resp_header(conn, "content-disposition")
|
||||
assert disposition == "attachment; filename=\"disposition\""
|
||||
end
|
||||
|
||||
test "with inline_content_types: false honours an explicit attachment_name opt", %{
|
||||
conn: conn
|
||||
} do
|
||||
disposition_headers_mock([
|
||||
{"content-type", "image/png"},
|
||||
{"content-length", "0"}
|
||||
])
|
||||
|
||||
conn =
|
||||
ReverseProxy.call(conn, "/disposition",
|
||||
inline_content_types: false,
|
||||
attachment_name: "custom.bin"
|
||||
)
|
||||
|
||||
[disposition] = Conn.get_resp_header(conn, "content-disposition")
|
||||
assert disposition == "attachment; filename=\"custom.bin\""
|
||||
end
|
||||
|
||||
test "forces bare inline for a whitelisted type with no MIME extension", %{
|
||||
conn: conn
|
||||
} do
|
||||
|
|
@ -469,36 +440,17 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
assert disposition == "inline"
|
||||
end
|
||||
|
||||
test "honours a custom inline_content_types whitelist", %{conn: conn} do
|
||||
# image/bmp is NOT in the default whitelist; with a custom whitelist
|
||||
# that includes it, the proxy should force inline.
|
||||
test "serves modern browser image types inline", %{conn: conn} do
|
||||
disposition_headers_mock([
|
||||
{"content-type", "image/bmp"},
|
||||
{"content-disposition", "attachment; filename=\"upstream.bmp\""},
|
||||
{"content-type", "image/webp"},
|
||||
{"content-length", "0"}
|
||||
])
|
||||
|
||||
conn =
|
||||
ReverseProxy.call(conn, "/disposition", inline_content_types: ["image/bmp"])
|
||||
conn = ReverseProxy.call(conn, "/disposition")
|
||||
|
||||
[disposition] = Conn.get_resp_header(conn, "content-disposition")
|
||||
assert String.starts_with?(disposition, "inline")
|
||||
assert String.ends_with?(disposition, "inline.bmp\"")
|
||||
end
|
||||
|
||||
test "treats a type outside the whitelist as attachment even on custom whitelist", %{
|
||||
conn: conn
|
||||
} do
|
||||
disposition_headers_mock([
|
||||
{"content-type", "image/png"},
|
||||
{"content-disposition", "attachment; filename=\"filename.png\""},
|
||||
{"content-length", "0"}
|
||||
])
|
||||
|
||||
conn =
|
||||
ReverseProxy.call(conn, "/disposition", inline_content_types: ["image/bmp"])
|
||||
|
||||
assert {"content-disposition", "attachment; filename=\"filename.png\""} in conn.resp_headers
|
||||
assert Conn.get_resp_header(conn, "content-disposition") == [
|
||||
"inline; filename=\"inline.webp\""
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,178 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentTypeSnifferTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentTypeSniffer
|
||||
|
||||
# @sniff_bytes must mirror the value in the module under test; we assert the
|
||||
# Range header that the sniffer sends, so the test should fail loudly if the
|
||||
# constant drifts.
|
||||
@sniff_bytes 8 * 1024
|
||||
@range_header "bytes=0-#{@sniff_bytes - 1}"
|
||||
|
||||
describe "sniff_image_type/1 with a real image body" do
|
||||
test "returns image/jpeg for a JPEG body" do
|
||||
url = "https://example.com/media/image.jpg"
|
||||
jpeg = File.read!("test/fixtures/image.jpg")
|
||||
|
||||
Tesla.Mock.mock(fn %{method: :get, url: ^url} ->
|
||||
%Tesla.Env{status: 200, body: jpeg}
|
||||
end)
|
||||
|
||||
assert {:ok, "image/jpeg"} = AttachmentTypeSniffer.sniff_image_type(url)
|
||||
end
|
||||
|
||||
test "returns image/png for a PNG body" do
|
||||
url = "https://example.com/media/image.png"
|
||||
png = File.read!("test/fixtures/image.png")
|
||||
|
||||
Tesla.Mock.mock(fn %{method: :get, url: ^url} ->
|
||||
%Tesla.Env{status: 200, body: png}
|
||||
end)
|
||||
|
||||
assert {:ok, "image/png"} = AttachmentTypeSniffer.sniff_image_type(url)
|
||||
end
|
||||
|
||||
test "returns image/gif for a GIF body" do
|
||||
url = "https://example.com/media/image.gif"
|
||||
gif = File.read!("test/fixtures/image.gif")
|
||||
|
||||
Tesla.Mock.mock(fn %{method: :get, url: ^url} ->
|
||||
%Tesla.Env{status: 200, body: gif}
|
||||
end)
|
||||
|
||||
assert {:ok, "image/gif"} = AttachmentTypeSniffer.sniff_image_type(url)
|
||||
end
|
||||
end
|
||||
|
||||
describe "sniff_image_type/1 sends a ranged request" do
|
||||
test "sends a Range header asking for the first chunk" do
|
||||
url = "https://example.com/media/ranged"
|
||||
jpeg = File.read!("test/fixtures/image.jpg")
|
||||
|
||||
Tesla.Mock.mock(fn
|
||||
%{method: :get, url: ^url, headers: headers} ->
|
||||
headers_map = Enum.into(headers, %{})
|
||||
assert headers_map["range"] == @range_header
|
||||
|
||||
%Tesla.Env{status: 200, body: jpeg}
|
||||
|
||||
%{method: :get, url: ^url} ->
|
||||
# If the sniffer did not send the Range header, fail explicitly.
|
||||
flunk("expected Range header to be sent with the request")
|
||||
end)
|
||||
|
||||
assert {:ok, "image/jpeg"} = AttachmentTypeSniffer.sniff_image_type(url)
|
||||
end
|
||||
end
|
||||
|
||||
describe "sniff_image_type/1 with a non-image body" do
|
||||
test "returns nil for plain text" do
|
||||
url = "https://example.com/media/note.txt"
|
||||
|
||||
Tesla.Mock.mock(fn %{method: :get, url: ^url} ->
|
||||
%Tesla.Env{status: 200, body: "just some plain text, not an image at all"}
|
||||
end)
|
||||
|
||||
assert {:ok, nil} = AttachmentTypeSniffer.sniff_image_type(url)
|
||||
end
|
||||
|
||||
test "returns nil for an HTML body" do
|
||||
url = "https://example.com/page"
|
||||
html = File.read!("test/fixtures/rel_me_anchor.html")
|
||||
|
||||
Tesla.Mock.mock(fn %{method: :get, url: ^url} ->
|
||||
%Tesla.Env{status: 200, body: html}
|
||||
end)
|
||||
|
||||
assert {:ok, nil} = AttachmentTypeSniffer.sniff_image_type(url)
|
||||
end
|
||||
|
||||
test "returns nil for an empty body" do
|
||||
url = "https://example.com/media/empty"
|
||||
|
||||
Tesla.Mock.mock(fn %{method: :get, url: ^url} ->
|
||||
%Tesla.Env{status: 200, body: ""}
|
||||
end)
|
||||
|
||||
assert {:ok, nil} = AttachmentTypeSniffer.sniff_image_type(url)
|
||||
end
|
||||
|
||||
test "returns nil for a nil body" do
|
||||
url = "https://example.com/media/nil-body"
|
||||
|
||||
Tesla.Mock.mock(fn %{method: :get, url: ^url} ->
|
||||
%Tesla.Env{status: 200, body: nil}
|
||||
end)
|
||||
|
||||
assert {:ok, nil} = AttachmentTypeSniffer.sniff_image_type(url)
|
||||
end
|
||||
end
|
||||
|
||||
describe "sniff_image_type/1 on HTTP failure" do
|
||||
test "returns nil on a 404" do
|
||||
url = "https://example.com/media/missing"
|
||||
|
||||
Tesla.Mock.mock(fn %{method: :get, url: ^url} ->
|
||||
%Tesla.Env{status: 404, body: ""}
|
||||
end)
|
||||
|
||||
assert {:ok, nil} = AttachmentTypeSniffer.sniff_image_type(url)
|
||||
end
|
||||
|
||||
test "returns nil on a 500" do
|
||||
url = "https://example.com/media/oops"
|
||||
|
||||
Tesla.Mock.mock(fn %{method: :get, url: ^url} ->
|
||||
%Tesla.Env{status: 500, body: "server error"}
|
||||
end)
|
||||
|
||||
assert {:ok, nil} = AttachmentTypeSniffer.sniff_image_type(url)
|
||||
end
|
||||
|
||||
test "returns nil on a 3xx (not in 200..299)" do
|
||||
url = "https://example.com/media/redirect"
|
||||
|
||||
Tesla.Mock.mock(fn %{method: :get, url: ^url} ->
|
||||
%Tesla.Env{status: 302, body: ""}
|
||||
end)
|
||||
|
||||
assert {:ok, nil} = AttachmentTypeSniffer.sniff_image_type(url)
|
||||
end
|
||||
|
||||
test "returns nil on a transport-level error" do
|
||||
url = "https://example.com/media/econnrefused"
|
||||
|
||||
Tesla.Mock.mock(fn %{method: :get, url: ^url} ->
|
||||
{:error, :econnrefused}
|
||||
end)
|
||||
|
||||
assert {:ok, nil} = AttachmentTypeSniffer.sniff_image_type(url)
|
||||
end
|
||||
|
||||
test "returns nil when the HTTP adapter raises" do
|
||||
url = "https://example.com/media/raises"
|
||||
|
||||
Tesla.Mock.mock(fn %{method: :get, url: ^url} ->
|
||||
raise "boom"
|
||||
end)
|
||||
|
||||
assert {:ok, nil} = AttachmentTypeSniffer.sniff_image_type(url)
|
||||
end
|
||||
end
|
||||
|
||||
describe "sniff_image_type/1 guard clauses" do
|
||||
test "returns nil for nil input without making a request" do
|
||||
# No Tesla mock is configured; if a request were made, Tesla.Mock would
|
||||
# raise and fail the test.
|
||||
assert {:ok, nil} = AttachmentTypeSniffer.sniff_image_type(nil)
|
||||
end
|
||||
|
||||
test "returns nil for an empty-string URL without making a request" do
|
||||
assert {:ok, nil} = AttachmentTypeSniffer.sniff_image_type("")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -215,226 +215,5 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidatorTest do
|
|||
AttachmentValidator.cast_and_validate(attachment)
|
||||
|> Ecto.Changeset.apply_action(:insert)
|
||||
end
|
||||
|
||||
test "sniffs image/jpeg for octet-stream attachments whose body is an image" do
|
||||
url = "https://example.com/media/no-extension/original"
|
||||
jpeg = File.read!("test/fixtures/image.jpg")
|
||||
|
||||
Tesla.Mock.mock(fn %{method: :get, url: ^url} ->
|
||||
%Tesla.Env{status: 200, body: jpeg}
|
||||
end)
|
||||
|
||||
attachment = %{
|
||||
"type" => "Document",
|
||||
"mediaType" => "application/octet-stream",
|
||||
"url" => [%{"type" => "Link", "href" => url, "mediaType" => "application/octet-stream"}]
|
||||
}
|
||||
|
||||
{:ok, attachment} =
|
||||
attachment
|
||||
|> AttachmentValidator.cast_and_validate()
|
||||
|> Ecto.Changeset.apply_action(:insert)
|
||||
|
||||
assert [%{href: ^url, mediaType: "image/jpeg"}] = attachment.url
|
||||
end
|
||||
|
||||
test "sniffs image/jpeg for attachments with a missing mediaType" do
|
||||
url = "https://example.com/media/missing-type/original"
|
||||
jpeg = File.read!("test/fixtures/image.jpg")
|
||||
|
||||
Tesla.Mock.mock(fn %{method: :get, url: ^url} ->
|
||||
%Tesla.Env{status: 200, body: jpeg}
|
||||
end)
|
||||
|
||||
attachment = %{
|
||||
"type" => "Document",
|
||||
"url" => [%{"type" => "Link", "href" => url}]
|
||||
}
|
||||
|
||||
{:ok, attachment} =
|
||||
attachment
|
||||
|> AttachmentValidator.cast_and_validate()
|
||||
|> Ecto.Changeset.apply_action(:insert)
|
||||
|
||||
assert [%{href: ^url, mediaType: "image/jpeg"}] = attachment.url
|
||||
end
|
||||
|
||||
test "leaves non-image octet-stream attachments as octet-stream" do
|
||||
url = "https://example.com/media/some-document"
|
||||
|
||||
Tesla.Mock.mock(fn %{method: :get, url: ^url} ->
|
||||
%Tesla.Env{status: 200, body: "just some plain text, not an image at all"}
|
||||
end)
|
||||
|
||||
attachment = %{
|
||||
"type" => "Document",
|
||||
"mediaType" => "application/octet-stream",
|
||||
"url" => [%{"type" => "Link", "href" => url, "mediaType" => "application/octet-stream"}]
|
||||
}
|
||||
|
||||
{:ok, attachment} =
|
||||
attachment
|
||||
|> AttachmentValidator.cast_and_validate()
|
||||
|> Ecto.Changeset.apply_action(:insert)
|
||||
|
||||
assert [%{mediaType: "application/octet-stream"}] = attachment.url
|
||||
end
|
||||
|
||||
test "does not sniff when the remote already provided a real mediaType" do
|
||||
# No Tesla mock is set up: if the validator tried to fetch, Tesla.Mock
|
||||
# would raise and fail the test. A real image type must be kept as-is.
|
||||
attachment = %{
|
||||
"type" => "Document",
|
||||
"mediaType" => "image/png",
|
||||
"url" => [
|
||||
%{"type" => "Link", "href" => "https://example.com/x", "mediaType" => "image/png"}
|
||||
]
|
||||
}
|
||||
|
||||
{:ok, attachment} =
|
||||
attachment
|
||||
|> AttachmentValidator.cast_and_validate()
|
||||
|> Ecto.Changeset.apply_action(:insert)
|
||||
|
||||
assert [%{mediaType: "image/png"}] = attachment.url
|
||||
end
|
||||
end
|
||||
|
||||
describe "fix_media_type fallbacks and sniffing" do
|
||||
test "uses mimeType as a fallback when mediaType is absent (real type, not sniffed)" do
|
||||
# No Tesla mock: if the validator tried to fetch, the test would fail.
|
||||
# A real image type coming from mimeType must be preserved as-is on the
|
||||
# outer attachment and on the synthesised url entry (string url form).
|
||||
attachment = %{
|
||||
"type" => "Document",
|
||||
"mimeType" => "image/png",
|
||||
"url" => "https://example.com/x.png"
|
||||
}
|
||||
|
||||
{:ok, attachment} =
|
||||
attachment
|
||||
|> AttachmentValidator.cast_and_validate()
|
||||
|> Ecto.Changeset.apply_action(:insert)
|
||||
|
||||
assert attachment.mediaType == "image/png"
|
||||
assert [%{mediaType: "image/png"}] = attachment.url
|
||||
end
|
||||
|
||||
test "prefers mediaType over mimeType when both are present" do
|
||||
attachment = %{
|
||||
"type" => "Document",
|
||||
"mediaType" => "image/gif",
|
||||
"mimeType" => "image/png",
|
||||
"url" => "https://example.com/x.gif"
|
||||
}
|
||||
|
||||
{:ok, attachment} =
|
||||
attachment
|
||||
|> AttachmentValidator.cast_and_validate()
|
||||
|> Ecto.Changeset.apply_action(:insert)
|
||||
|
||||
assert attachment.mediaType == "image/gif"
|
||||
assert [%{mediaType: "image/gif"}] = attachment.url
|
||||
end
|
||||
|
||||
test "sniffs when the url entry's mediaType is application/octet-stream and the body is an image" do
|
||||
# The outer attachment mediaType is not sniffed (no top-level href); only
|
||||
# the url entry is. We assert only on the url entry.
|
||||
url = "https://example.com/media/mime-type-octet"
|
||||
jpeg = File.read!("test/fixtures/image.jpg")
|
||||
|
||||
Tesla.Mock.mock(fn %{method: :get, url: ^url} ->
|
||||
%Tesla.Env{status: 200, body: jpeg}
|
||||
end)
|
||||
|
||||
attachment = %{
|
||||
"type" => "Document",
|
||||
"mediaType" => "application/octet-stream",
|
||||
"url" => [%{"type" => "Link", "href" => url, "mediaType" => "application/octet-stream"}]
|
||||
}
|
||||
|
||||
{:ok, attachment} =
|
||||
attachment
|
||||
|> AttachmentValidator.cast_and_validate()
|
||||
|> Ecto.Changeset.apply_action(:insert)
|
||||
|
||||
assert [%{mediaType: "image/jpeg"}] = attachment.url
|
||||
end
|
||||
|
||||
test "sniffs when mediaType is the empty string and the body is an image" do
|
||||
url = "https://example.com/media/empty-mediatype"
|
||||
png = File.read!("test/fixtures/image.png")
|
||||
|
||||
Tesla.Mock.mock(fn %{method: :get, url: ^url} ->
|
||||
%Tesla.Env{status: 200, body: png}
|
||||
end)
|
||||
|
||||
attachment = %{
|
||||
"type" => "Document",
|
||||
"mediaType" => "",
|
||||
"url" => [%{"type" => "Link", "href" => url, "mediaType" => ""}]
|
||||
}
|
||||
|
||||
{:ok, attachment} =
|
||||
attachment
|
||||
|> AttachmentValidator.cast_and_validate()
|
||||
|> Ecto.Changeset.apply_action(:insert)
|
||||
|
||||
assert [%{mediaType: "image/png"}] = attachment.url
|
||||
end
|
||||
|
||||
test "falls back to the declared type when the sniffer HTTP request fails" do
|
||||
url = "https://example.com/media/sniff-404"
|
||||
|
||||
Tesla.Mock.mock(fn %{method: :get, url: ^url} ->
|
||||
%Tesla.Env{status: 404, body: ""}
|
||||
end)
|
||||
|
||||
attachment = %{
|
||||
"type" => "Document",
|
||||
"mediaType" => "application/octet-stream",
|
||||
"url" => [%{"type" => "Link", "href" => url, "mediaType" => "application/octet-stream"}]
|
||||
}
|
||||
|
||||
{:ok, attachment} =
|
||||
attachment
|
||||
|> AttachmentValidator.cast_and_validate()
|
||||
|> Ecto.Changeset.apply_action(:insert)
|
||||
|
||||
assert [%{mediaType: "application/octet-stream"}] = attachment.url
|
||||
end
|
||||
|
||||
test "sniffs each url entry independently" do
|
||||
jpeg_url = "https://example.com/media/a"
|
||||
octet_url = "https://example.com/media/b"
|
||||
jpeg = File.read!("test/fixtures/image.jpg")
|
||||
|
||||
Tesla.Mock.mock(fn
|
||||
%{method: :get, url: ^jpeg_url} ->
|
||||
%Tesla.Env{status: 200, body: jpeg}
|
||||
|
||||
%{method: :get, url: ^octet_url} ->
|
||||
%Tesla.Env{status: 200, body: "definitely not an image"}
|
||||
end)
|
||||
|
||||
attachment = %{
|
||||
"type" => "Document",
|
||||
"mediaType" => "application/octet-stream",
|
||||
"url" => [
|
||||
%{"type" => "Link", "href" => jpeg_url, "mediaType" => "application/octet-stream"},
|
||||
%{"type" => "Link", "href" => octet_url, "mediaType" => "application/octet-stream"}
|
||||
]
|
||||
}
|
||||
|
||||
{:ok, attachment} =
|
||||
attachment
|
||||
|> AttachmentValidator.cast_and_validate()
|
||||
|> Ecto.Changeset.apply_action(:insert)
|
||||
|
||||
assert [
|
||||
%{href: ^jpeg_url, mediaType: "image/jpeg"},
|
||||
%{href: ^octet_url, mediaType: "application/octet-stream"}
|
||||
] = attachment.url
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -668,6 +668,23 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
|
|||
assert expected == StatusView.render("attachment.json", %{attachment: object})
|
||||
assert_schema(expected, "Attachment", api_spec)
|
||||
end
|
||||
|
||||
test "uses the ActivityStreams image type when the MIME type is generic" do
|
||||
object = %{
|
||||
"type" => "Image",
|
||||
"url" => [
|
||||
%{
|
||||
"mediaType" => "application/octet-stream",
|
||||
"href" => "https://example.com/extensionless"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
attachment = StatusView.render("attachment.json", %{attachment: object})
|
||||
|
||||
assert attachment.type == "image"
|
||||
assert attachment.pleroma.mime_type == "application/octet-stream"
|
||||
end
|
||||
end
|
||||
|
||||
test "put the url advertised in the Activity in to the url attribute" do
|
||||
|
|
|
|||
|
|
@ -71,7 +71,10 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
|
|||
|
||||
test "it performs ReverseProxy.call with valid signature", %{conn: conn, url: url} do
|
||||
with_mock Pleroma.ReverseProxy,
|
||||
call: fn _conn, _url, _opts -> %Conn{status: :success} end do
|
||||
call: fn _conn, _url, opts ->
|
||||
assert opts[:sniff_content_type]
|
||||
%Conn{status: :success}
|
||||
end do
|
||||
assert %Conn{status: :success} = get(conn, url)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue