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/fallback

Hello world

12345not 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 == "

Hello world

" 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