6.9 KiB · text History 6d24830
defmodule FeedPug.PromEx.Plugins.LibraryTest do
  @moduledoc """
  The plugin's two pollers run raw SQL against tables no context module owns,
  so nothing else in the suite would notice these queries drifting from the
  schema. Each test drives the poll function directly and reads the telemetry
  it emits.
  """
  use FeedPug.DataCase, async: true

  import FeedPug.AccountsFixtures

  alias FeedPug.Feeds
  alias FeedPug.Groups
  alias FeedPug.PromEx.Plugins.Library

  @events [
    [:feed_pug, :library, :users],
    [:feed_pug, :library, :feeds],
    [:feed_pug, :library, :following],
    [:feed_pug, :library, :rows, :estimate],
    [:feed_pug, :library, :corpus]
  ]

  setup do
    ref = make_ref()
    test = self()

    :telemetry.attach_many(
      "library-test-#{inspect(ref)}",
      @events,
      fn event, measurements, metadata, _ ->
        send(test, {:metric, event, measurements, metadata})
      end,
      nil
    )

    on_exit(fn -> :telemetry.detach("library-test-#{inspect(ref)}") end)
    :ok
  end

  # Every emission for one event, as {metadata, measurements} pairs. Drains the
  # mailbox rather than matching one message, because the zero-filled metrics
  # emit once per label value.
  defp collect(event) do
    receive do
      {:metric, ^event, measurements, metadata} -> [{metadata, measurements} | collect(event)]
    after
      0 -> []
    end
  end

  defp single(event) do
    Library.execute_library_metrics()
    [{_metadata, measurements}] = collect(event)
    measurements
  end

  defp group(scope, path) do
    Enum.find(Groups.list_groups(scope), &(&1.path == path))
  end

  defp add_feed(scope, path, url) do
    {:ok, group_feed} = Groups.add_feed_to_group(scope, group(scope, path), url)
    Feeds.get_feed!(group_feed.feed_id)
  end

  defp add_items(feed, count) do
    entries = for n <- 1..count, do: %{guid: "#{feed.id}-#{n}", title: "item #{n}"}
    {_n, items} = Feeds.store_items(feed, entries)
    items
  end

  describe "users" do
    test "splits on confirmation and reports both states even when one is empty" do
      user_fixture()
      user_fixture()

      Library.execute_library_metrics()

      counts =
        [:feed_pug, :library, :users]
        |> collect()
        |> Map.new(fn {%{state: state}, %{users: users}} -> {state, users} end)

      assert counts == %{"confirmed" => 2, "pending" => 0}
    end

    test "counts an unconfirmed registration as pending" do
      unconfirmed_user_fixture()

      Library.execute_library_metrics()

      counts =
        [:feed_pug, :library, :users]
        |> collect()
        |> Map.new(fn {%{state: state}, %{users: users}} -> {state, users} end)

      assert counts == %{"confirmed" => 0, "pending" => 1}
    end
  end

  describe "feeds" do
    test "reports every status in the schema, not only the ones in use" do
      scope = user_scope_fixture()
      feed = add_feed(scope, "blogs", "https://a.example/feed.xml")
      {:ok, _} = Feeds.update_feed(feed, %{status: "failed"})

      Library.execute_library_metrics()

      counts =
        [:feed_pug, :library, :feeds]
        |> collect()
        |> Map.new(fn {%{status: status}, %{feeds: feeds}} -> {status, feeds} end)

      assert counts == %{"active" => 0, "paused" => 0, "failed" => 1, "archived" => 0}
    end
  end

  describe "following" do
    test "separates placements, distinct subscriptions and distinct feeds" do
      scope = user_scope_fixture()
      other = user_scope_fixture()

      # One feed filed under two of the same user's groups: two placements,
      # one subscription, one feed. A second user following it adds a
      # subscription without adding a feed.
      shared = "https://shared.example/feed.xml"
      add_feed(scope, "blogs", shared)
      add_feed(scope, "news", shared)
      add_feed(other, "blogs", shared)
      add_feed(scope, "comics", "https://solo.example/feed.xml")

      assert %{
               placements: 4,
               subscriptions: 3,
               subscribed_feeds: 2,
               group_follows: 0
             } = single([:feed_pug, :library, :following])
    end

    test "counts a follow of another user's group" do
      scope = user_scope_fixture()
      other = user_scope_fixture()
      {:ok, _follow} = Groups.follow_group(other, group(scope, "blogs"))

      assert %{group_follows: 1} = single([:feed_pug, :library, :following])
    end
  end

  describe "corpus and read ratio" do
    test "counts items and reads, and divides only over what is subscribed" do
      scope = user_scope_fixture()
      followed = add_feed(scope, "blogs", "https://followed.example/feed.xml")

      # A feed nobody has in a group: its items are stored and fetched, and
      # they are not readable by anyone.
      {:ok, orphan} = Feeds.upsert_feed_by_url("https://orphan.example/feed.xml")

      [a, b, _c, _d] = add_items(followed, 4)
      [e | _] = add_items(orphan, 2)

      :ok = Feeds.mark_read(scope.user.id, a.id)
      :ok = Feeds.mark_read(scope.user.id, b.id)
      # A read left behind on a feed the user does not subscribe to. It counts
      # as a read marker and must not count towards the ratio, which would
      # otherwise be able to exceed 1.
      :ok = Feeds.mark_read(scope.user.id, e.id)

      Library.execute_corpus_metrics()

      assert [{_meta, measurements}] = collect([:feed_pug, :library, :corpus])

      assert %{
               items: 6,
               item_reads: 3,
               readable_items: 4,
               subscribed_item_reads: 2,
               read_ratio: ratio
             } = measurements

      # A float, not a Decimal: `sum()` over a count comes back as numeric, and
      # dividing by the Decimal that produces raises instead.
      assert is_float(ratio)
      assert ratio == 0.5
    end

    test "counts the same item once per subscriber" do
      scope = user_scope_fixture()
      other = user_scope_fixture()
      shared = "https://shared.example/feed.xml"
      feed = add_feed(scope, "blogs", shared)
      add_feed(other, "blogs", shared)
      [a, _b] = add_items(feed, 2)

      :ok = Feeds.mark_read(scope.user.id, a.id)

      Library.execute_corpus_metrics()
      [{_meta, measurements}] = collect([:feed_pug, :library, :corpus])

      assert %{items: 2, readable_items: 4, subscribed_item_reads: 1, read_ratio: 0.25} =
               measurements
    end

    test "reports a ratio of zero rather than dividing by nothing" do
      Library.execute_corpus_metrics()
      [{_meta, measurements}] = collect([:feed_pug, :library, :corpus])

      assert %{items: 0, item_reads: 0, readable_items: 0, read_ratio: +0.0} = measurements
    end
  end

  describe "row estimates" do
    test "reports both unbounded tables on every poll" do
      Library.execute_library_metrics()

      tables =
        [:feed_pug, :library, :rows, :estimate]
        |> collect()
        |> Enum.map(fn {%{table: table}, _} -> table end)
        |> Enum.sort()

      assert tables == ["item_reads", "items"]
    end
  end
end