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