From 9dd0556072e117dcfc0210b639c5f6bbe0d1778b Mon Sep 17 00:00:00 2001 From: Moises Macia Date: Thu, 16 Apr 2026 23:23:08 +0000 Subject: [PATCH] add tests --- test/feed_reader/rdf_test.exs | 126 ++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 test/feed_reader/rdf_test.exs diff --git a/test/feed_reader/rdf_test.exs b/test/feed_reader/rdf_test.exs new file mode 100644 index 0000000..17f3d44 --- /dev/null +++ b/test/feed_reader/rdf_test.exs @@ -0,0 +1,126 @@ +defmodule FeedReader.RDFTest do + use ExUnit.Case, async: true + + alias FeedReader.{Feed, Formats.RDF, Item} + + @xml """ + + Example Newshttps://example.com/TechTech + <b>First</b>/firstSummaryWriter2025-03-06T14:00:00+00:00 + Title only + + """ + + test "implements the format contract and emits feed before normalized RDF items" do + assert RDF.id() == :rdf + assert RDF.matches_prefix?("") + assert RDF.handler() == FeedReader.Formats.RDF.Handler + sink = fn event -> send(self(), event) end + assert {:ok, _} = parse(@xml, sink) + + assert_receive {:feed, + %Feed{title: "Example", description: "News", type: :rdf, categories: ["Tech"]}} + + assert_receive {:item, + %Item{ + title: "First", + uuid: "https://example.com/first", + canonical_url: "https://example.com/first", + content_summary: "Summary", + author: "Writer", + published_at: ~U[2025-03-06 14:00:00Z] + }} + + assert_receive {:item, %Item{title: "Title only", content_full: nil, content_summary: nil}} + end + + test "enforces shared SAX limits and reports malformed XML" do + sink = fn _event -> :ok end + + assert {:ok, %{error: {:resource_limit, :max_item_bytes, 1}}} = + parse(@xml, sink, max_item_bytes: 1) + + assert {:ok, %{error: {:resource_limit, :max_item_bytes, 10}}} = + parse("First", sink, + max_item_bytes: 10 + ) + + assert {:ok, %{error: {:resource_limit, :max_item_bytes, 4}}} = + parse("", sink, max_item_bytes: 4) + + assert {:ok, %{error: {:resource_limit, :max_feed_metadata_bytes, 1}}} = + parse(@xml, sink, max_feed_metadata_bytes: 1) + + assert {:error, _reason} = parse("", sink) + end + + test "emits fallback metadata before the first item when a channel is absent" do + sink = fn event -> send(self(), event) end + + assert {:ok, _} = parse("Orphan", sink) + assert_receive {:feed, %Feed{title: nil, type: :rdf}} + assert_receive {:item, %Item{title: "Orphan"}} + end + + test "normalizes feed title fallbacks and non-map channel data" do + assert %Feed{ + title: "Description fallback", + description: "Description fallback", + feed_url: "https://example.com/feed.rdf", + html_url: "https://example.com/", + categories: [] + } = + RDF.normalize_feed( + %{ + channel: %{ + "description" => "Description fallback", + "link" => "https://example.com/" + } + }, + feed_url: "https://example.com/feed.rdf" + ) + + assert %Feed{title: "https://example.com/", html_url: "https://example.com/"} = + RDF.normalize_feed(%{channel: %{"link" => "https://example.com/"}}, []) + + assert %Feed{title: nil, categories: []} = RDF.normalize_feed(%{channel: nil}, []) + end + + test "normalizes blank titles, invalid dates, and empty language input" do + before = DateTime.utc_now(:second) + + assert {:ok, + %Item{title: nil, content_summary: nil, published_at: published_at, language: :en}} = + RDF.normalize_item(%{"title" => " ", "dc:date" => "not a date"}, %Feed{}, []) + + assert DateTime.compare(published_at, before) in [:eq, :gt] + end + + test "stops on SAX accounting errors and rejects orphan events" do + handler = RDF.handler() + sink = fn _event -> :ok end + limited = RDF.new_parser_state(sink, max_xml_events: 0) + + assert {:stop, %{error: {:resource_limit, :max_xml_events, 0}}} = + handler.handle_event(:start_element, {"root", []}, limited) + + assert {:stop, %{error: {:resource_limit, :max_xml_events, 0}}} = + handler.handle_event(:start_document, [], limited) + + state = RDF.new_parser_state(sink, []) + + assert {:stop, %{error: :invalid_sax_event}} = + handler.handle_event(:characters, "text", state) + + assert {:stop, %{error: :invalid_sax_event}} = + handler.handle_event(:end_element, "item", state) + + state = %{state | limits: %{state.limits | depth: 1}} + + assert {:stop, %{error: :invalid_sax_event}} = + handler.handle_event(:end_element, "item", state) + end + + defp parse(xml, sink, opts \\ []), + do: Saxy.parse_string(xml, RDF.handler(), RDF.new_parser_state(sink, opts)) +end