4.0 KiB · text History 4b6fe3b
defmodule OhMyExOhIrcd.SightingsTest do
  use OhMyExOhIrcd.DataCase, async: true

  import OhMyExOhIrcd.AccountsFixtures

  alias OhMyExOhIrcd.Sightings
  alias OhMyExOhIrcd.Sightings.Sighting

  defp at(days_ago, now \\ ~U[2026-10-27 12:00:00Z]),
    do: DateTime.add(now, -days_ago * 86_400, :second)

  defp nick(prefix), do: "#{prefix}#{System.unique_integer([:positive])}"

  describe "record/1 keying" do
    test "an anonymous nick is one row per downcased nick; first_seen_at is kept" do
      n = nick("Anon")

      {:ok, first} =
        Sightings.record(%{nick: n, host: "a.cloak", real_ip: "10.0.0.1", at: at(3)})

      {:ok, second} =
        Sightings.record(%{
          nick: String.upcase(n),
          host: "b.cloak",
          real_ip: "10.0.0.2",
          at: at(1)
        })

      assert first.id == second.id
      assert second.nick == String.upcase(n)
      assert second.nick_key == String.downcase(n)
      assert second.first_seen_at == at(3)
      assert second.last_seen_at == at(1)
      assert second.host == "b.cloak"
      assert second.real_ip == "10.0.0.2"
      assert is_nil(second.user_id)
    end

    test "a registered account is one row whatever nick it used" do
      user = user_fixture()

      {:ok, a} = Sightings.record(%{nick: nick("old"), user_id: user.id, host: "h", at: at(2)})
      new_nick = nick("new")
      {:ok, b} = Sightings.record(%{nick: new_nick, user_id: user.id, host: "h2", at: at(1)})

      assert a.id == b.id
      assert b.nick == new_nick
      assert b.first_seen_at == at(2)
      assert Repo.aggregate(from(s in Sighting, where: s.user_id == ^user.id), :count) == 1
    end

    test "the same nick, anonymous and registered, is two rows" do
      n = nick("shared")
      user = user_fixture()

      {:ok, anon} = Sightings.record(%{nick: n})
      {:ok, reg} = Sightings.record(%{nick: n, user_id: user.id})

      refute anon.id == reg.id
      assert reg.user_id == user.id
    end

    test "last_seen_at never moves backwards; a known IP and quit reason are kept" do
      n = nick("order")
      {:ok, _} = Sightings.record(%{nick: n, real_ip: "10.1.1.1", quit_reason: "bye", at: at(1)})
      {:ok, row} = Sightings.record(%{nick: n, at: at(5)})

      assert row.last_seen_at == at(1)
      assert row.real_ip == "10.1.1.1"
      assert row.last_quit_reason == "bye"
    end

    test "service nicks are skipped" do
      assert :skipped = Sightings.record(%{nick: OhMyExOhIrcd.IRC.ChanServ.nick()})
      assert :skipped = Sightings.record(%{nick: "imgserv"})
    end
  end

  describe "prune/1" do
    test "removes old unregistered rows, keeps recent ones and every registered row" do
      now = ~U[2026-10-27 12:00:00Z]
      user = user_fixture()

      {:ok, old_anon} = Sightings.record(%{nick: nick("stale"), at: at(91, now)})
      {:ok, recent_anon} = Sightings.record(%{nick: nick("fresh"), at: at(89, now)})

      {:ok, old_reg} =
        Sightings.record(%{nick: nick("loyal"), user_id: user.id, at: at(400, now)})

      assert Sightings.prune(now) >= 1

      refute Repo.get(Sighting, old_anon.id)
      assert Repo.get(Sighting, recent_anon.id)
      assert Repo.get(Sighting, old_reg.id)
    end
  end

  describe "list_recent/2" do
    test "newest first, filtered by type, with the account preloaded" do
      user = user_fixture()
      {:ok, anon} = Sightings.record(%{nick: nick("la"), at: at(2)})
      {:ok, reg} = Sightings.record(%{nick: nick("lr"), user_id: user.id, at: at(1)})

      ids = Sightings.list_recent(:all) |> Enum.map(& &1.id)
      assert Enum.find_index(ids, &(&1 == reg.id)) < Enum.find_index(ids, &(&1 == anon.id))

      [only_reg] = Sightings.list_recent(:registered) |> Enum.filter(&(&1.id == reg.id))
      assert only_reg.user.email == user.email
      refute Enum.any?(Sightings.list_recent(:registered), &(&1.id == anon.id))
      refute Enum.any?(Sightings.list_recent(:unregistered), &(&1.id == reg.id))
      assert Enum.any?(Sightings.list_recent(:unregistered), &(&1.id == anon.id))
    end
  end
end