add tests
This commit is contained in:
1 parent
2df2aedb68
commit
0cddbfc76f
3 files changed
+172
-1
No files matched your search
@@ -0,0 +1,150 @@
|
|||||||
|
defmodule FeedReader.DownloaderTest do
|
||||||
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
|
alias FeedReader.Downloader
|
||||||
|
|
||||||
|
test "downloads decoded bytes, computes a lowercase MD5, and releases its file" do
|
||||||
|
payload = "<rss>feed</rss>"
|
||||||
|
response = response([payload], [{"ETag", "\"v1\""}, {"content-encoding", "identity"}])
|
||||||
|
|
||||||
|
assert {:ok, %{path: path, fetch: fetch}} = download(response, max_decoded_bytes: :invalid)
|
||||||
|
assert File.read!(path) == payload
|
||||||
|
assert fetch.checksum == Base.encode16(:crypto.hash(:md5, payload), case: :lower)
|
||||||
|
assert fetch.etag == "\"v1\""
|
||||||
|
assert fetch.status == 200
|
||||||
|
assert fetch.final_url == "https://example.com/final.xml"
|
||||||
|
assert :ok = Downloader.close(%{path: path})
|
||||||
|
refute File.exists?(path)
|
||||||
|
assert :ok = Downloader.close(%{path: path})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "decompresses gzip before writing and checksumming" do
|
||||||
|
payload = "<rss><channel><title>feed</title></channel></rss>"
|
||||||
|
compressed = :zlib.gzip(payload)
|
||||||
|
midpoint = div(byte_size(compressed), 2)
|
||||||
|
|
||||||
|
chunks = [
|
||||||
|
binary_part(compressed, 0, midpoint),
|
||||||
|
binary_part(compressed, midpoint, byte_size(compressed) - midpoint)
|
||||||
|
]
|
||||||
|
|
||||||
|
response = response(chunks, [{"content-encoding", "gzip"}])
|
||||||
|
|
||||||
|
assert {:ok, %{path: path, fetch: fetch}} = download(response)
|
||||||
|
assert File.read!(path) == payload
|
||||||
|
assert fetch.checksum == Base.encode16(:crypto.hash(:md5, payload), case: :lower)
|
||||||
|
Downloader.close(%{path: path})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "decompresses raw deflate content" do
|
||||||
|
payload = "<rss>deflate</rss>"
|
||||||
|
compressed = deflate(payload)
|
||||||
|
|
||||||
|
assert {:ok, %{path: path, fetch: fetch}} =
|
||||||
|
download(response([compressed], [{"content-encoding", "deflate"}]))
|
||||||
|
|
||||||
|
assert File.read!(path) == payload
|
||||||
|
assert fetch.checksum == checksum(payload)
|
||||||
|
Downloader.close(%{path: path})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "maps 304 without creating a temporary file" do
|
||||||
|
before = download_files()
|
||||||
|
assert {:ok, :not_modified} = download(response([], [], 304))
|
||||||
|
assert download_files() == before
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns not changed after checksum and removes the temporary file" do
|
||||||
|
payload = "unchanged"
|
||||||
|
before = download_files()
|
||||||
|
|
||||||
|
assert {:ok, :not_changed} =
|
||||||
|
download(response([payload], []), previous_checksum: checksum(payload))
|
||||||
|
|
||||||
|
assert download_files() == before
|
||||||
|
end
|
||||||
|
|
||||||
|
test "enforces compressed and decoded byte limits and cleans failed downloads" do
|
||||||
|
before = download_files()
|
||||||
|
|
||||||
|
assert {:error, {:resource_limit, :max_compressed_bytes, 2}} =
|
||||||
|
download(response(["long"], []), max_compressed_bytes: 2)
|
||||||
|
|
||||||
|
assert {:error, {:resource_limit, :max_decoded_bytes, 2}} =
|
||||||
|
download(response(["long"], []), max_decoded_bytes: 2)
|
||||||
|
|
||||||
|
assert {:error, {:resource_limit, :max_temp_bytes, 2}} =
|
||||||
|
download(response(["long"], []), max_temp_bytes: 2)
|
||||||
|
|
||||||
|
compressed = :zlib.gzip(String.duplicate("a", 100))
|
||||||
|
|
||||||
|
assert {:error, {:resource_limit, :max_expansion_ratio, 1}} =
|
||||||
|
download(response([compressed], [{"content-encoding", "gzip"}]),
|
||||||
|
max_expansion_ratio: 1
|
||||||
|
)
|
||||||
|
|
||||||
|
assert download_files() == before
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns stable errors for invalid URLs, statuses, transport errors, and chunks" do
|
||||||
|
assert {:error, :invalid_url} = Downloader.download(:invalid, [])
|
||||||
|
assert {:error, {:http_status, 418}} = download(response([], [], 418))
|
||||||
|
assert {:error, :offline} = download(response([], []), request_result: {:error, :offline})
|
||||||
|
assert {:error, :invalid_chunk} = download(response([:not_binary], []))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "rejects unknown encodings and malformed compressed bodies, cleaning temp files" do
|
||||||
|
before = download_files()
|
||||||
|
|
||||||
|
assert {:error, :unsupported_content_encoding} =
|
||||||
|
download(response(["body"], [{"content-encoding", "br, gzip"}]))
|
||||||
|
|
||||||
|
assert {:error, :invalid_compressed_body} =
|
||||||
|
download(response(["not gzip"], [{"content-encoding", "gzip"}]))
|
||||||
|
|
||||||
|
assert download_files() == before
|
||||||
|
end
|
||||||
|
|
||||||
|
test "converts exceptions while enumerating response chunks to download errors" do
|
||||||
|
before = download_files()
|
||||||
|
chunks = Stream.map(["chunk"], fn _chunk -> raise "chunk read failed" end)
|
||||||
|
|
||||||
|
assert {:error, {:download_failed, %RuntimeError{message: "chunk read failed"}}} =
|
||||||
|
download(response(chunks, []))
|
||||||
|
|
||||||
|
assert download_files() == before
|
||||||
|
end
|
||||||
|
|
||||||
|
defp download(response, opts \\ []) do
|
||||||
|
Downloader.download(
|
||||||
|
"https://example.com/feed.xml",
|
||||||
|
[
|
||||||
|
http_client: FeedReader.TestSupport.TransportClient,
|
||||||
|
response: response,
|
||||||
|
test_pid: self()
|
||||||
|
] ++
|
||||||
|
opts
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp response(body, headers, status \\ 200) do
|
||||||
|
%{status: status, headers: headers, final_url: "https://example.com/final.xml", body: body}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp checksum(payload), do: Base.encode16(:crypto.hash(:md5, payload), case: :lower)
|
||||||
|
|
||||||
|
defp deflate(payload) do
|
||||||
|
zlib = :zlib.open()
|
||||||
|
:ok = :zlib.deflateInit(zlib, :default, :deflated, -15, 8, :default)
|
||||||
|
compressed = :zlib.deflate(zlib, payload, :finish) |> IO.iodata_to_binary()
|
||||||
|
:ok = :zlib.deflateEnd(zlib)
|
||||||
|
:zlib.close(zlib)
|
||||||
|
compressed
|
||||||
|
end
|
||||||
|
|
||||||
|
defp download_files do
|
||||||
|
System.tmp_dir!()
|
||||||
|
|> Path.join("feed_reader-download-*.xml")
|
||||||
|
|> Path.wildcard()
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -47,6 +47,19 @@ defmodule FeedReader.TransportTest do
|
|||||||
assert Enum.join(response.body) == "body"
|
assert Enum.join(response.body) == "body"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "sends previous ETags verbatim and exposes a 304 response" do
|
||||||
|
Req.Test.expect(__MODULE__, fn conn ->
|
||||||
|
assert Plug.Conn.get_req_header(conn, "if-none-match") == ["W/\"exact\""]
|
||||||
|
Plug.Conn.send_resp(conn, 304, "")
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert {:ok, %{status: 304, body: []}} =
|
||||||
|
ReqTransport.request("https://example.com/feed",
|
||||||
|
previous_etag: "W/\"exact\"",
|
||||||
|
http_options: [plug: {Req.Test, __MODULE__}]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
test "follows redirects and reports the final URL" do
|
test "follows redirects and reports the final URL" do
|
||||||
Req.Test.expect(__MODULE__, 2, fn conn ->
|
Req.Test.expect(__MODULE__, 2, fn conn ->
|
||||||
case conn.request_path do
|
case conn.request_path do
|
||||||
|
|||||||
@@ -6,6 +6,14 @@ defmodule FeedReader.TestSupport.TransportClient do
|
|||||||
@impl true
|
@impl true
|
||||||
def request(url, opts) do
|
def request(url, opts) do
|
||||||
send(opts[:test_pid], {:transport_request, url})
|
send(opts[:test_pid], {:transport_request, url})
|
||||||
{:ok, %{status: 200, headers: [], final_url: url, body: ["test"]}}
|
|
||||||
|
case Keyword.get(opts, :request_result) do
|
||||||
|
nil ->
|
||||||
|
{:ok,
|
||||||
|
Keyword.get(opts, :response, %{status: 200, headers: [], final_url: url, body: ["test"]})}
|
||||||
|
|
||||||
|
result ->
|
||||||
|
result
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
Reference in new issue
Block a user