From 2424a989bc0197dfbebd4999ecb9dbf8e65cc2da Mon Sep 17 00:00:00 2001 From: Moises Macia Date: Mon, 18 May 2026 21:29:10 +0000 Subject: [PATCH] add tests --- test/feed_reader/collector_test.exs | 130 ++++++++++++++++++++++++ test/support/external_format_handler.ex | 1 + 2 files changed, 131 insertions(+) create mode 100644 test/feed_reader/collector_test.exs diff --git a/test/feed_reader/collector_test.exs b/test/feed_reader/collector_test.exs new file mode 100644 index 0000000..bcdc03f --- /dev/null +++ b/test/feed_reader/collector_test.exs @@ -0,0 +1,130 @@ +defmodule FeedReader.CollectorTest do + use ExUnit.Case, async: true + + alias FeedReader.{Feed, Item, ParseResult} + + @xml ~s(ExampleFirstoneSecondtwo) + + test "collects binary XML through the local event stream" do + assert {:ok, %ParseResult{feed: %Feed{title: "Example"}, items: items}} = + FeedReader.parse_binary(@xml, max_items: 2) + + assert [%Item{uuid: "one"}, %Item{uuid: "two"}] = items + end + + test "collects chunk enumerables without requiring a path" do + chunks = String.graphemes(@xml) + + assert {:ok, %ParseResult{items: [%Item{uuid: "one"}, %Item{uuid: "two"}]}} = + FeedReader.parse_stream(chunks, max_items: 2) + end + + test "collects caller-owned files and preserves them" do + path = write_temp(@xml) + on_exit(fn -> File.rm(path) end) + + assert {:ok, %ParseResult{items: items}} = FeedReader.parse_file(path, max_items: 2) + assert length(items) == 2 + assert File.read!(path) == @xml + end + + test "requires a positive max_items for every collector" do + assert {:error, {:invalid_option, :max_items}} = FeedReader.parse_binary(@xml) + assert {:error, {:invalid_option, :max_items}} = FeedReader.parse_file("missing.xml") + assert {:error, {:invalid_option, :max_items}} = FeedReader.parse_stream([]) + assert {:error, {:invalid_option, :max_items}} = FeedReader.parse_binary(@xml, max_items: 0) + assert {:error, {:invalid_option, :max_items}} = FeedReader.parse_stream([], max_items: :all) + end + + test "propagates local source errors" do + assert {:error, :enoent} = FeedReader.parse_file("/path/that/does/not/exist", max_items: 1) + end + + test "filters before counting and collecting" do + filter = fn %Item{uuid: uuid} -> uuid == "two" end + + assert {:ok, %ParseResult{items: [%Item{uuid: "two"}]}} = + FeedReader.parse_binary(@xml, max_items: 1, item_filter: filter) + end + + test "reports an item beyond the bound instead of returning a partial result" do + assert {:error, {:max_items_exceeded, 1}} = FeedReader.parse_binary(@xml, max_items: 1) + end + + test "continues to the terminal parser event before returning a malformed-tail error" do + assert {:error, _reason} = FeedReader.parse_binary(@xml <> "") + on_exit(fn -> File.rm(path) end) + + assert {:error, {:invalid_option, :item_filter}} = + FeedReader.parse_file(path, + format: :external_test, + formats: [FeedReader.TestSupport.ExternalFormat], + max_items: 1, + item_filter: :not_a_function + ) + end + + test "rejects collection without feed metadata" do + path = write_temp("") + on_exit(fn -> File.rm(path) end) + + assert {:error, :feed_metadata_missing} = + FeedReader.parse_file(path, + format: :external_test, + formats: [FeedReader.TestSupport.ExternalFormat], + max_items: 1 + ) + end + + test "handles duplicate metadata, ignored events, and parser errors" do + feed = %Feed{title: "Custom", type: :rss} + item = %Item{uuid: "custom"} + path = write_temp("") + on_exit(fn -> File.rm(path) end) + + opts = [ + format: :external_test, + formats: [FeedReader.TestSupport.ExternalFormat], + max_items: 1, + collector_events: [{:feed, feed}, :ignored, {:item, item}, {:feed, feed}] + ] + + assert {:ok, %ParseResult{feed: ^feed, items: [^item]}} = FeedReader.parse_file(path, opts) + + assert {:error, :custom_parser_error} = + FeedReader.parse_file( + path, + Keyword.put(opts, :collector_events, [{:error, :custom_parser_error}]) + ) + end + + test "reports an enumerable failure and still cleans its temporary source" do + chunks = + Stream.resource(fn -> :ok end, fn _state -> raise "chunk failure" end, fn _ -> :ok end) + + assert {:error, {:stream_write_failed, %RuntimeError{message: "chunk failure"}}} = + FeedReader.parse_stream(chunks, max_items: 1) + end + + defp write_temp(content) do + path = + Path.join( + System.tmp_dir!(), + "feed_reader-collector-#{System.unique_integer([:positive])}.xml" + ) + + File.write!(path, content) + path + end +end diff --git a/test/support/external_format_handler.ex b/test/support/external_format_handler.ex index 22b8591..f3fd89d 100644 --- a/test/support/external_format_handler.ex +++ b/test/support/external_format_handler.ex @@ -7,6 +7,7 @@ defmodule FeedReader.TestSupport.ExternalFormatHandler do @impl true def handle_event(:start_document, _data, %{opts: opts} = state) do if Keyword.get(opts, :emit_event), do: emit_event(state) + Enum.each(Keyword.get(opts, :collector_events, []), fn event -> state.event_sink.(event) end) {:ok, state} end