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

  import OhMyExOhIrcd.AccountsFixtures

  alias OhMyExOhIrcd.Channels
  alias OhMyExOhIrcd.Scrobbles

  setup do
    user = user_fixture()
    chan = "#scr-#{System.unique_integer([:positive])}"
    {:ok, _rec} = Channels.register(chan, user)
    {:ok, user: user, chan: chan}
  end

  describe "config" do
    test "ensure_config requires a registered channel" do
      assert {:error, :not_registered} = Scrobbles.ensure_config("#nope-#{System.unique_integer()}")
    end

    test "ensure_config creates an enabled, not-joined config", %{chan: chan} do
      assert {:ok, cfg} = Scrobbles.ensure_config(chan)
      assert cfg.enabled
      refute cfg.joined
      assert cfg.format =~ "%who%"
    end

    test "ensure_config is idempotent", %{chan: chan} do
      {:ok, a} = Scrobbles.ensure_config(chan)
      {:ok, b} = Scrobbles.ensure_config(chan)
      assert a.id == b.id
    end

    test "update_config + toggles", %{chan: chan} do
      {:ok, _} = Scrobbles.set_enabled(chan, false)
      {:ok, _} = Scrobbles.set_format(chan, "%who%: %track%")
      {:ok, _} = Scrobbles.set_rate_limit(chan, 5)
      {:ok, _} = Scrobbles.set_restrict(chan, true)

      cfg = Scrobbles.get_config(chan)
      refute cfg.enabled
      assert cfg.format == "%who%: %track%"
      assert cfg.rate_limit_seconds == 5
      assert cfg.restrict_to_allowlist
    end

    test "format strips control chars", %{chan: chan} do
      {:ok, cfg} = Scrobbles.set_format(chan, "%who%\r\nPRIVMSG #evil :x")
      refute cfg.format =~ "\n"
      refute cfg.format =~ "\r"
    end

    test "allowlist add/remove is downcased and deduped", %{chan: chan} do
      {:ok, _} = Scrobbles.add_allow(chan, "GMP")
      {:ok, _} = Scrobbles.add_allow(chan, "gmp")
      assert Scrobbles.get_config(chan).allowlist == ["gmp"]

      {:ok, _} = Scrobbles.remove_allow(chan, "Gmp")
      assert Scrobbles.get_config(chan).allowlist == []
    end

    test "list_joined_channels only returns joined", %{chan: chan} do
      {:ok, _} = Scrobbles.ensure_config(chan)
      refute chan in Scrobbles.list_joined_channels()

      {:ok, _} = Scrobbles.set_joined(chan, true)
      assert chan in Scrobbles.list_joined_channels()
    end
  end

  describe "tokens" do
    test "mint + verify round-trips channel, who and user", %{chan: chan, user: user} do
      assert {:ok, display, token} = Scrobbles.mint_token(chan, user, "gmp", "laptop")
      assert is_binary(display)
      assert token.label == "laptop"
      assert token.who == "gmp"
      assert token.user_id == user.id

      assert {:ok, %{channel: channel, who: who, user_id: uid}} = Scrobbles.verify_token(display)
      assert channel.name == String.downcase(chan)
      assert who == "gmp"
      assert uid == user.id
    end

    test "mint requires a non-blank who", %{chan: chan, user: user} do
      assert {:error, :blank_who} = Scrobbles.mint_token(chan, user, "   ", "x")
    end

    test "verify bumps last_used_at", %{chan: chan, user: user} do
      {:ok, display, token} = Scrobbles.mint_token(chan, user, "gmp", nil)
      assert token.last_used_at == nil
      {:ok, _} = Scrobbles.verify_token(display)
      [refreshed] = Scrobbles.list_tokens(chan)
      assert refreshed.last_used_at
    end

    test "revoked tokens no longer verify", %{chan: chan, user: user} do
      {:ok, display, token} = Scrobbles.mint_token(chan, user, "gmp", nil)
      :ok = Scrobbles.revoke_token(chan, token.id)
      assert :error = Scrobbles.verify_token(display)
      assert Scrobbles.list_tokens(chan) == []
    end

    test "garbage tokens return :error", %{chan: _chan} do
      assert :error = Scrobbles.verify_token("not-a-real-token")
      assert :error = Scrobbles.verify_token("!!!not base64!!!")
    end
  end

  describe "history" do
    test "record + recent newest-first with limit", %{chan: chan} do
      %{id: id} = Channels.get(chan)
      for n <- 1..5, do: {:ok, _} = Scrobbles.record_scrobble(id, %{who: "w", artist: "a", track: "t#{n}"})

      recent = Scrobbles.recent(chan, 3)
      assert length(recent) == 3
      assert hd(recent).track == "t5"
    end
  end

  describe "request logs" do
    test "log + recent newest-first", %{chan: chan} do
      %{id: id} = Channels.get(chan)
      {:ok, _} = Scrobbles.log_request(id, %{who: "gmp", status: "ok"})
      {:ok, _} = Scrobbles.log_request(id, %{who: "gmp", status: "ignored", detail: "rate_limited"})

      logs = Scrobbles.recent_logs(chan, 10)
      assert length(logs) == 2
      assert hd(logs).status == "ignored"
      assert hd(logs).detail == "rate_limited"
    end

    test "prunes beyond retention", %{chan: chan} do
      %{id: id} = Channels.get(chan)
      for n <- 1..205, do: {:ok, _} = Scrobbles.log_request(id, %{who: "w", status: "ok", detail: "#{n}"})

      # Retention caps stored rows at 200.
      assert length(Scrobbles.recent_logs(chan, 1000)) == 200
    end
  end
end