defmodule GitGud.AvatarsTest do
@moduledoc """
Uploaded avatars, and the identicon that stands in when there isn't
one.
The property worth defending is that `render/1` always answers with an
image. Every call site — the profile page, an `<img>`, the F-Droid
repository icon — is written against that, so a caller that has to
handle "no avatar" is a caller that will get it wrong somewhere.
"""
use GitGud.DataCase, async: false
import GitGud.ForgeFixtures
alias GitGud.Avatars
# Minimal valid images, by magic bytes. The context sniffs the body
# rather than trusting a header, so these have to be real signatures.
@png <<137, 80, 78, 71, 13, 10, 26, 10>> <> :binary.copy(<<0>>, 32)
@jpeg <<0xFF, 0xD8, 0xFF, 0xE0>> <> :binary.copy(<<0>>, 32)
@gif "GIF89a" <> :binary.copy(<<0>>, 32)
@webp "RIFF" <> <<40::little-32>> <> "WEBP" <> :binary.copy(<<0>>, 32)
setup do
{user, repo} = repository_fixture()
%{user: user, repo: repo}
end
describe "render/1 without an upload" do
test "returns the identicon rather than nothing", %{user: user} do
assert {:ok, "image/png", png} = Avatars.render(user)
assert <<137, 80, 78, 71, 13, 10, 26, 10, _::binary>> = png
end
test "is the same grid the profile page draws", %{user: user} do
{:ok, _, png} = Avatars.render(user)
assert png == GitGud.Avatars.Identicon.png(Avatars.seed(user))
end
test "repositories and users get different faces", %{user: user, repo: repo} do
{:ok, _, for_user} = Avatars.render(user)
{:ok, _, for_repo} = Avatars.render(repo)
refute for_user == for_repo
end
end
describe "put/2" do
test "stores an upload and render/1 serves it back", %{user: user} do
assert {:ok, avatar} = Avatars.put(user, @png)
assert avatar.content_type == "image/png"
assert avatar.size == byte_size(@png)
assert {:ok, "image/png", @png} = Avatars.render(user)
end
test "detects the type from the bytes, not from a claim", %{user: user} do
for {body, expected} <- [
{@png, "image/png"},
{@jpeg, "image/jpeg"},
{@gif, "image/gif"},
{@webp, "image/webp"}
] do
{:ok, avatar} = Avatars.put(user, body)
assert avatar.content_type == expected
end
end
test "refuses something that isn't an image", %{user: user} do
# Including the case that matters: a browser sniffs the body, so
# "it said PNG" is how an HTML file gets served from our origin.
assert {:error, :unsupported_type} = Avatars.put(user, "<html><script>alert(1)</script>")
assert {:error, :unsupported_type} = Avatars.put(user, "<svg xmlns='...'></svg>")
end
test "refuses an empty upload", %{user: user} do
assert {:error, :empty} = Avatars.put(user, "")
end
test "refuses one that's too large", %{user: user} do
oversized = :binary.copy(<<0>>, Avatars.max_bytes() + 1)
assert {:error, :too_large} = Avatars.put(user, oversized)
end
test "replacing keeps one row and one object", %{user: user} do
{:ok, first} = Avatars.put(user, @png)
{:ok, second} = Avatars.put(user, @jpeg)
assert first.id == second.id
assert second.content_type == "image/jpeg"
# The old object goes only after the row points at the new one.
assert {:error, _} = GitGud.Storage.get(first.storage_bucket, first.storage_key)
assert {:ok, @jpeg} = GitGud.Storage.get(second.storage_bucket, second.storage_key)
end
test "re-uploading identical bytes doesn't delete the object", %{user: user} do
{:ok, first} = Avatars.put(user, @png)
{:ok, second} = Avatars.put(user, @png)
# Same digest means the same key — discarding "the old one" here
# would remove the object the new row points at.
assert first.storage_key == second.storage_key
assert {:ok, "image/png", @png} = Avatars.render(user)
end
test "works for repositories too", %{repo: repo} do
assert {:ok, _} = Avatars.put(repo, @png)
assert {:ok, "image/png", @png} = Avatars.render(repo)
end
end
describe "delete/1" do
test "hands the identicon back", %{user: user} do
{:ok, avatar} = Avatars.put(user, @png)
assert :ok = Avatars.delete(user)
assert {:ok, "image/png", png} = Avatars.render(user)
assert png == GitGud.Avatars.Identicon.png(Avatars.seed(user))
assert {:error, _} = GitGud.Storage.get(avatar.storage_bucket, avatar.storage_key)
end
test "is fine with no avatar", %{user: user} do
assert :ok = Avatars.delete(user)
end
end
describe "url/1" do
test "carries a version so a replacement is a different URL", %{user: user} do
before = Avatars.url(user)
{:ok, _} = Avatars.put(user, @png)
after_upload = Avatars.url(user)
assert before =~ "/u/#{user.handle}/avatar?v="
refute before == after_upload
end
test "a repository's URL is under the repository", %{repo: repo, user: user} do
assert Avatars.url(repo) =~ "/r/#{user.handle}/#{repo.name}/avatar?v="
end
end
describe "a missing object" do
test "falls back rather than serving a broken image", %{user: user} do
{:ok, avatar} = Avatars.put(user, @png)
# The row says there is an upload; the store disagrees.
:ok = GitGud.Storage.delete(avatar.storage_bucket, avatar.storage_key)
assert {:ok, "image/png", png} = Avatars.render(user)
assert png == GitGud.Avatars.Identicon.png(Avatars.seed(user))
end
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
5.5 KiB · text
History
b868907