5.1 KiB · text History 4b6fe3b
defmodule OhMyExOhIrcd.ImagesTest do
  use OhMyExOhIrcd.DataCase, async: false

  import OhMyExOhIrcd.AccountsFixtures

  alias OhMyExOhIrcd.Channels
  alias OhMyExOhIrcd.Images
  alias OhMyExOhIrcd.Images.Image

  # 1x1 transparent PNG.
  @png Base.decode64!(
         "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M8AAAMBAQDJ/pLvAAAAAElFTkSuQmCC"
       )

  setup do
    # Isolate uploads to a temp dir and restore app env afterwards.
    tmp = Path.join(System.tmp_dir!(), "imgtest-#{System.unique_integer([:positive])}")
    prev = Application.get_env(:oh_my_ex_oh_ircd, :imgserv)
    Application.put_env(:oh_my_ex_oh_ircd, :imgserv, Keyword.put(prev, :upload_dir, tmp))

    on_exit(fn ->
      Application.put_env(:oh_my_ex_oh_ircd, :imgserv, prev)
      File.rm_rf(tmp)
    end)

    user = user_fixture()
    chan = "#img-#{System.unique_integer([:positive])}"
    {:ok, _} = Channels.register(chan, user)
    {:ok, user: user, chan: chan, tmp: tmp}
  end

  defp store(chan, attrs \\ %{}) do
    base = %{who: "gmp", content_type: "image/png", byte_size: byte_size(@png)}
    Images.store_upload(chan, Map.merge(base, attrs), {:binary, @png})
  end

  describe "storage" do
    test "stores bytes on disk and records metadata", %{chan: chan} do
      assert {:ok, %Image{} = img} = store(chan)
      assert img.who == "gmp"
      assert img.content_type == "image/png"
      assert String.ends_with?(img.filename, ".png")
      assert File.exists?(Images.path_for(img))
      assert File.read!(Images.path_for(img)) == @png
    end

    test "rejects a disallowed content type", %{chan: chan} do
      assert {:error, :bad_type} = store(chan, %{content_type: "application/pdf"})
    end

    test "rejects an oversized upload", %{chan: chan} do
      assert {:error, :too_large} = store(chan, %{byte_size: Images.max_bytes() + 1})
    end

    test "lookup by filename + recent", %{chan: chan} do
      {:ok, img} = store(chan)
      assert Images.get_image_by_filename(img.filename).id == img.id
      assert hd(Images.recent(chan, 5)).id == img.id
    end

    test "delete removes row and file", %{chan: chan} do
      {:ok, img} = store(chan)
      path = Images.path_for(img)
      :ok = Images.delete_image(chan, img.id)
      refute File.exists?(path)
      assert Images.get_image_by_filename(img.filename) == nil
    end
  end

  describe "pinning (per-user budget)" do
    test "pin then unpin updates the user's pinned byte total", %{chan: chan, user: user} do
      {:ok, img} = store(chan)
      assert Images.pinned_bytes_for_user(user.id) == 0

      assert {:ok, pinned} = Images.pin_image(chan, img.id, user)
      assert pinned.pinned
      assert pinned.pinned_by_user_id == user.id
      assert Images.pinned_bytes_for_user(user.id) == img.byte_size

      assert {:ok, _} = Images.unpin_image(chan, img.id)
      assert Images.pinned_bytes_for_user(user.id) == 0
    end

    test "refuses to pin beyond the per-user budget", %{chan: chan, user: user} do
      prev = Application.get_env(:oh_my_ex_oh_ircd, :imgserv)
      Application.put_env(:oh_my_ex_oh_ircd, :imgserv, Keyword.put(prev, :pin_budget_mb, 0))
      on_exit(fn -> Application.put_env(:oh_my_ex_oh_ircd, :imgserv, prev) end)

      {:ok, img} = store(chan)
      assert {:error, :over_budget} = Images.pin_image(chan, img.id, user)
      refute Images.get_image_by_filename(img.filename).pinned
    end
  end

  describe "retention GC" do
    defp backdate(%Image{} = img, days) do
      old =
        DateTime.utc_now() |> DateTime.add(-days * 86_400, :second) |> DateTime.truncate(:second)

      Repo.update_all(from(i in Image, where: i.id == ^img.id), set: [inserted_at: old])
    end

    test "removes expired non-pinned images and their files", %{chan: chan} do
      {:ok, img} = store(chan)
      path = Images.path_for(img)
      backdate(img, 40)

      assert Images.gc() >= 1
      refute File.exists?(path)
      assert Repo.get(Image, img.id) == nil
    end

    test "keeps pinned images even when expired", %{chan: chan, user: user} do
      {:ok, img} = store(chan)
      {:ok, _} = Images.pin_image(chan, img.id, user)
      backdate(img, 40)

      Images.gc()
      assert Repo.get(Image, img.id)
      assert File.exists?(Images.path_for(img))
    end

    test "honours a per-channel retention override", %{chan: chan} do
      {:ok, _} = Images.set_retention_days(chan, 1)
      {:ok, img} = store(chan)
      backdate(img, 2)

      assert Images.gc() >= 1
      assert Repo.get(Image, img.id) == nil
    end

    test "keeps images within the window", %{chan: chan} do
      {:ok, img} = store(chan)
      backdate(img, 5)
      Images.gc()
      assert Repo.get(Image, img.id)
    end
  end

  describe "tokens + config" do
    test "mint/verify round-trips channel + who + user", %{chan: chan, user: user} do
      assert {:ok, display, _t} = Images.mint_token(chan, user, "gmp", "cam")
      assert {:ok, %{channel: c, who: "gmp", user_id: uid}} = Images.verify_token(display)
      assert c.name == String.downcase(chan)
      assert uid == user.id
    end

    test "retention override validation rejects nonsense", %{chan: chan} do
      assert {:error, _} = Images.set_retention_days(chan, -3)
    end
  end
end