defmodule FeedReader.AtomTest do
use ExUnit.Case, async: true
alias FeedReader.{Feed, Formats.Atom, Item}
@xml ~s(NewsUpdatesStorytag:example.com,2025:1Writer2025-02-24T17:00:05Z<p>Full text</p>Brief)
@xhtml ~s(Feedhttps://example.com/fallback12345not a date)
test "normalizes feed metadata and entry fields" do
assert {:halt, _, _} = parse(@xml)
assert_receive {:feed, %Feed{title: "News", type: :atom, categories: ["Tech"]}}
assert_receive {:item, %Item{content_full: "
Full text
"}}
assert %Feed{html_url: nil} = Atom.normalize_feed(%{feed: %{}}, [])
assert {:halt, _, _} = parse(@xhtml)
assert_receive {:feed, %Feed{html_url: "https://example.com/fallback"}}
assert_receive {:item, %Item{content_full: xhtml, content_summary: nil}}
assert xhtml == ""
assert_receive {:item, %Item{title: "12345", content_summary: "12345"}}
end
test "preserves relative links when no absolute feed base is available" do
raw = %{"title" => "Story", "link" => [%{"href" => "/story", "rel" => "alternate"}]}
assert {:ok, %Item{canonical_url: "/story"}} =
Atom.normalize_item(raw, %Feed{html_url: "/"}, [])
assert {:ok, %Item{canonical_url: "https://example.com/story"}} =
Atom.normalize_item(
raw,
%Feed{html_url: "/", feed_url: "https://example.com/feed.xml"},
[]
)
end
test "joins multiple author names in source order" do
raw = %{
"title" => "Story",
"author" => [%{"name" => "Alice"}, %{"name" => "Bob"}]
}
assert {:ok, %Item{author: "Alice, Bob"}} = Atom.normalize_item(raw, %Feed{}, [])
end
test "accounts for parser failures and closes empty feeds" do
handler = Atom.handler()
sink = fn event -> send(self(), event) end
limited = Atom.new_parser_state(fn _ -> :ok end, max_xml_events: 0)
assert {:stop, %{error: {:resource_limit, :max_xml_events, 0}}} =
handler.handle_event(:start_document, [], limited)
assert {:stop, %{error: {:resource_limit, :max_xml_events, 0}}} =
handler.handle_event(:end_element, "feed", limited)
empty = Atom.new_parser_state(fn _ -> :ok end, [])
assert {:stop, %{error: :invalid_sax_event}} =
handler.handle_event(:characters, "text", empty)
assert {:stop, %{error: :invalid_sax_event}} =
handler.handle_event(:end_element, "feed", %{
empty
| limits: %{empty.limits | depth: 1}
})
frame = %{name: "entry", attrs: %{}, children: %{}, text: "", markup: []}
limited_item = %{
empty
| limits: %{empty.limits | scope: :item, max_item_bytes: 0},
frames: [frame]
}
assert {:stop, %{error: {:resource_limit, :max_item_bytes, 0}}} =
handler.handle_event(:characters, "x", limited_item)
state = Atom.new_parser_state(fn _ -> :ok end, max_item_bytes: 1)
assert {:ok, state} = handler.handle_event(:start_element, {"feed", []}, state)
assert {:stop, %{error: {:resource_limit, :max_item_bytes, 1}}} =
handler.handle_event(:start_element, {"entry", []}, state)
assert {:halt, _, _} = Saxy.parse_string("", handler, Atom.new_parser_state(sink, []))
assert_receive {:feed, %Feed{type: :atom}}
assert {:ok, _} =
Saxy.parse_string("", handler, Atom.new_parser_state(fn _ -> :ok end, []))
end
defp parse(xml),
do: Saxy.parse_string(xml, Atom.handler(), Atom.new_parser_state(&send(self(), &1), []))
end