- Schema: alt, caption, tags fields on images table with metadata changeset - Context: list_images with filters, find_usages, used_image_ids, delete_with_cleanup - Admin UI: /admin/media with grid view, upload, filters, detail panel, metadata editing - Block editor: :image field type for image_text and content_body blocks - Page renderer: image_id resolution with legacy URL fallback - Mobile: bottom sheet detail panel with slide-up animation - CSS: uses correct --t-* admin theme tokens, admin-badge colour variants Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
356 lines
11 KiB
Elixir
356 lines
11 KiB
Elixir
defmodule Berrypod.MediaTest do
|
|
use Berrypod.DataCase, async: false
|
|
|
|
alias Berrypod.Media
|
|
|
|
@valid_attrs %{
|
|
image_type: "logo",
|
|
filename: "logo.png",
|
|
content_type: "image/png",
|
|
file_size: 1024,
|
|
data: <<137, 80, 78, 71>>
|
|
}
|
|
|
|
@svg_attrs %{
|
|
image_type: "logo",
|
|
filename: "logo.svg",
|
|
content_type: "image/svg+xml",
|
|
file_size: 512,
|
|
data: "<svg xmlns=\"http://www.w3.org/2000/svg\"><circle r=\"10\"/></svg>"
|
|
}
|
|
|
|
describe "upload_image/1" do
|
|
test "uploads an image with valid attributes" do
|
|
assert {:ok, image} = Media.upload_image(@valid_attrs)
|
|
assert image.image_type == "logo"
|
|
assert image.filename == "logo.png"
|
|
assert image.content_type == "image/png"
|
|
assert image.is_svg == false
|
|
end
|
|
|
|
test "detects and stores SVG content" do
|
|
assert {:ok, image} = Media.upload_image(@svg_attrs)
|
|
assert image.is_svg == true
|
|
assert image.svg_content == @svg_attrs.data
|
|
end
|
|
|
|
test "validates required fields" do
|
|
assert {:error, changeset} = Media.upload_image(%{})
|
|
assert "can't be blank" in errors_on(changeset).image_type
|
|
assert "can't be blank" in errors_on(changeset).filename
|
|
end
|
|
|
|
test "validates image_type inclusion" do
|
|
attrs = Map.put(@valid_attrs, :image_type, "invalid")
|
|
assert {:error, changeset} = Media.upload_image(attrs)
|
|
assert "is invalid" in errors_on(changeset).image_type
|
|
end
|
|
|
|
test "accepts icon image type" do
|
|
attrs = Map.put(@valid_attrs, :image_type, "icon")
|
|
assert {:ok, image} = Media.upload_image(attrs)
|
|
assert image.image_type == "icon"
|
|
end
|
|
|
|
test "validates file size" do
|
|
attrs = Map.put(@valid_attrs, :file_size, 10_000_000)
|
|
assert {:error, changeset} = Media.upload_image(attrs)
|
|
assert "must be less than 5000000" in errors_on(changeset).file_size
|
|
end
|
|
end
|
|
|
|
describe "get_image/1" do
|
|
test "returns image by id" do
|
|
{:ok, image} = Media.upload_image(@valid_attrs)
|
|
assert ^image = Media.get_image(image.id)
|
|
end
|
|
|
|
test "returns nil for nonexistent id" do
|
|
assert Media.get_image(Ecto.UUID.generate()) == nil
|
|
end
|
|
end
|
|
|
|
describe "get_logo/0 and get_header/0" do
|
|
test "get_logo returns a logo" do
|
|
{:ok, logo} = Media.upload_image(@valid_attrs)
|
|
|
|
result = Media.get_logo()
|
|
assert result.id == logo.id
|
|
assert result.image_type == "logo"
|
|
end
|
|
|
|
test "get_header returns most recent header" do
|
|
attrs = Map.put(@valid_attrs, :image_type, "header")
|
|
{:ok, header} = Media.upload_image(attrs)
|
|
|
|
result = Media.get_header()
|
|
assert result.id == header.id
|
|
end
|
|
|
|
test "get_logo returns nil when no logos exist" do
|
|
assert Media.get_logo() == nil
|
|
end
|
|
end
|
|
|
|
describe "delete_image/1" do
|
|
test "deletes an image" do
|
|
{:ok, image} = Media.upload_image(@valid_attrs)
|
|
assert {:ok, _} = Media.delete_image(image)
|
|
assert Media.get_image(image.id) == nil
|
|
end
|
|
end
|
|
|
|
describe "list_images_by_type/1" do
|
|
test "lists all images of a specific type" do
|
|
{:ok, logo1} = Media.upload_image(@valid_attrs)
|
|
{:ok, logo2} = Media.upload_image(@valid_attrs)
|
|
{:ok, _header} = Media.upload_image(Map.put(@valid_attrs, :image_type, "header"))
|
|
|
|
logos = Media.list_images_by_type("logo")
|
|
assert length(logos) == 2
|
|
assert Enum.any?(logos, &(&1.id == logo1.id))
|
|
assert Enum.any?(logos, &(&1.id == logo2.id))
|
|
end
|
|
|
|
test "returns empty list when no images of type exist" do
|
|
assert Media.list_images_by_type("product") == []
|
|
end
|
|
end
|
|
|
|
describe "get_icon/0" do
|
|
test "returns the most recent icon image" do
|
|
attrs = Map.put(@valid_attrs, :image_type, "icon")
|
|
{:ok, icon} = Media.upload_image(attrs)
|
|
|
|
result = Media.get_icon()
|
|
assert result.id == icon.id
|
|
assert result.image_type == "icon"
|
|
end
|
|
|
|
test "returns nil when no icons exist" do
|
|
assert Media.get_icon() == nil
|
|
end
|
|
end
|
|
|
|
describe "favicon variants" do
|
|
defp create_source_image do
|
|
{:ok, image} = Media.upload_image(@valid_attrs)
|
|
image
|
|
end
|
|
|
|
test "stores and retrieves favicon variants" do
|
|
image = create_source_image()
|
|
|
|
{:ok, variants} =
|
|
Media.store_favicon_variants(%{
|
|
source_image_id: image.id,
|
|
png_32: <<1, 2, 3>>,
|
|
png_180: <<4, 5, 6>>,
|
|
png_192: <<7, 8, 9>>,
|
|
png_512: <<10, 11, 12>>
|
|
})
|
|
|
|
assert variants.png_32 == <<1, 2, 3>>
|
|
|
|
fetched = Media.get_favicon_variants()
|
|
assert fetched.id == variants.id
|
|
assert fetched.png_32 == <<1, 2, 3>>
|
|
assert fetched.png_512 == <<10, 11, 12>>
|
|
end
|
|
|
|
test "replaces existing variants on store" do
|
|
image = create_source_image()
|
|
|
|
{:ok, first} =
|
|
Media.store_favicon_variants(%{
|
|
source_image_id: image.id,
|
|
png_32: <<1>>,
|
|
png_180: <<2>>,
|
|
png_192: <<3>>,
|
|
png_512: <<4>>
|
|
})
|
|
|
|
{:ok, second} =
|
|
Media.store_favicon_variants(%{
|
|
source_image_id: image.id,
|
|
png_32: <<5>>,
|
|
png_180: <<6>>,
|
|
png_192: <<7>>,
|
|
png_512: <<8>>
|
|
})
|
|
|
|
assert second.id != first.id
|
|
fetched = Media.get_favicon_variants()
|
|
assert fetched.id == second.id
|
|
assert fetched.png_32 == <<5>>
|
|
end
|
|
|
|
test "returns nil when no variants exist" do
|
|
assert Media.get_favicon_variants() == nil
|
|
end
|
|
end
|
|
|
|
# ── Media library functions ──────────────────────────────────────
|
|
|
|
describe "upload_image/1 with media type" do
|
|
test "accepts media image type" do
|
|
attrs = Map.put(@valid_attrs, :image_type, "media")
|
|
assert {:ok, image} = Media.upload_image(attrs)
|
|
assert image.image_type == "media"
|
|
end
|
|
|
|
test "stores alt, caption, and tags on upload" do
|
|
attrs =
|
|
@valid_attrs
|
|
|> Map.put(:alt, "A scenic landscape")
|
|
|> Map.put(:caption, "Taken at sunset")
|
|
|> Map.put(:tags, "nature, landscape")
|
|
|
|
assert {:ok, image} = Media.upload_image(attrs)
|
|
assert image.alt == "A scenic landscape"
|
|
assert image.caption == "Taken at sunset"
|
|
assert image.tags == "nature, landscape"
|
|
end
|
|
end
|
|
|
|
describe "list_images/1" do
|
|
test "lists all images without BLOBs" do
|
|
{:ok, _} = Media.upload_image(@valid_attrs)
|
|
{:ok, _} = Media.upload_image(Map.put(@valid_attrs, :image_type, "header"))
|
|
|
|
images = Media.list_images()
|
|
assert length(images) == 2
|
|
assert Enum.all?(images, &is_nil(&1.data))
|
|
end
|
|
|
|
test "filters by type" do
|
|
{:ok, _} = Media.upload_image(@valid_attrs)
|
|
{:ok, _} = Media.upload_image(Map.put(@valid_attrs, :image_type, "header"))
|
|
|
|
logos = Media.list_images(type: "logo")
|
|
assert length(logos) == 1
|
|
assert hd(logos).image_type == "logo"
|
|
end
|
|
|
|
test "filters by search on filename" do
|
|
{:ok, _} = Media.upload_image(@valid_attrs)
|
|
{:ok, _} = Media.upload_image(Map.put(@valid_attrs, :filename, "banner.png"))
|
|
|
|
results = Media.list_images(search: "banner")
|
|
assert length(results) == 1
|
|
assert hd(results).filename == "banner.png"
|
|
end
|
|
|
|
test "filters by search on alt text" do
|
|
{:ok, _} = Media.upload_image(Map.merge(@valid_attrs, %{alt: "Mountain sunrise"}))
|
|
{:ok, _} = Media.upload_image(Map.merge(@valid_attrs, %{alt: "Beach sunset"}))
|
|
|
|
results = Media.list_images(search: "Mountain")
|
|
assert length(results) == 1
|
|
assert hd(results).alt == "Mountain sunrise"
|
|
end
|
|
|
|
test "filters by tag" do
|
|
{:ok, _} = Media.upload_image(Map.merge(@valid_attrs, %{tags: "hero, homepage"}))
|
|
{:ok, _} = Media.upload_image(Map.merge(@valid_attrs, %{tags: "product, gallery"}))
|
|
|
|
results = Media.list_images(tag: "hero")
|
|
assert length(results) == 1
|
|
assert hd(results).tags == "hero, homepage"
|
|
end
|
|
|
|
test "returns empty list with no matches" do
|
|
{:ok, _} = Media.upload_image(@valid_attrs)
|
|
assert Media.list_images(type: "product") == []
|
|
end
|
|
end
|
|
|
|
describe "update_image_metadata/2" do
|
|
test "updates alt, caption, and tags" do
|
|
{:ok, image} = Media.upload_image(@valid_attrs)
|
|
|
|
assert {:ok, updated} =
|
|
Media.update_image_metadata(image, %{
|
|
alt: "New alt text",
|
|
caption: "A caption",
|
|
tags: "tag1, tag2"
|
|
})
|
|
|
|
assert updated.alt == "New alt text"
|
|
assert updated.caption == "A caption"
|
|
assert updated.tags == "tag1, tag2"
|
|
end
|
|
|
|
test "does not affect image data fields" do
|
|
{:ok, image} = Media.upload_image(@valid_attrs)
|
|
original_type = image.image_type
|
|
|
|
{:ok, updated} = Media.update_image_metadata(image, %{alt: "Updated"})
|
|
assert updated.image_type == original_type
|
|
end
|
|
end
|
|
|
|
describe "find_usages/1" do
|
|
test "returns empty list for unreferenced image" do
|
|
{:ok, image} = Media.upload_image(@valid_attrs)
|
|
assert Media.find_usages(image.id) == []
|
|
end
|
|
|
|
test "finds theme setting usages" do
|
|
{:ok, image} = Media.upload_image(@valid_attrs)
|
|
Berrypod.Settings.update_theme_settings(%{logo_image_id: image.id})
|
|
|
|
usages = Media.find_usages(image.id)
|
|
assert [%{type: :theme, label: "Logo"}] = usages
|
|
end
|
|
|
|
test "finds favicon variant usage" do
|
|
{:ok, image} = Media.upload_image(@valid_attrs)
|
|
|
|
Media.store_favicon_variants(%{
|
|
source_image_id: image.id,
|
|
png_32: <<1>>,
|
|
png_180: <<2>>,
|
|
png_192: <<3>>,
|
|
png_512: <<4>>
|
|
})
|
|
|
|
usages = Media.find_usages(image.id)
|
|
assert [%{type: :favicon, label: "Favicon source"}] = usages
|
|
end
|
|
end
|
|
|
|
describe "used_image_ids/0" do
|
|
test "collects IDs from theme settings" do
|
|
{:ok, image} = Media.upload_image(@valid_attrs)
|
|
Berrypod.Settings.update_theme_settings(%{logo_image_id: image.id})
|
|
|
|
ids = Media.used_image_ids()
|
|
assert MapSet.member?(ids, image.id)
|
|
end
|
|
|
|
test "returns empty set when nothing is referenced" do
|
|
{:ok, _} = Media.upload_image(@valid_attrs)
|
|
ids = Media.used_image_ids()
|
|
assert MapSet.size(ids) == 0
|
|
end
|
|
end
|
|
|
|
describe "delete_with_cleanup/1" do
|
|
test "deletes an unreferenced image" do
|
|
{:ok, image} = Media.upload_image(@valid_attrs)
|
|
assert {:ok, _} = Media.delete_with_cleanup(image)
|
|
assert Media.get_image(image.id) == nil
|
|
end
|
|
|
|
test "refuses to delete an image that is in use" do
|
|
{:ok, image} = Media.upload_image(@valid_attrs)
|
|
Berrypod.Settings.update_theme_settings(%{logo_image_id: image.id})
|
|
|
|
assert {:error, :in_use, usages} = Media.delete_with_cleanup(image)
|
|
assert length(usages) == 1
|
|
assert Media.get_image(image.id) != nil
|
|
end
|
|
end
|
|
end
|