135 lines
3.9 KiB
Go
135 lines
3.9 KiB
Go
|
|
package avatar
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"encoding/binary"
|
||
|
|
"errors"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"testing"
|
||
|
|
)
|
||
|
|
|
||
|
|
func png(t *testing.T, w, h uint32, pad int) []byte {
|
||
|
|
t.Helper()
|
||
|
|
var b bytes.Buffer
|
||
|
|
b.WriteString("\x89PNG\r\n\x1a\n")
|
||
|
|
b.Write([]byte{0, 0, 0, 13})
|
||
|
|
b.WriteString("IHDR")
|
||
|
|
binary.Write(&b, binary.BigEndian, w)
|
||
|
|
binary.Write(&b, binary.BigEndian, h)
|
||
|
|
b.Write(make([]byte, pad))
|
||
|
|
return b.Bytes()
|
||
|
|
}
|
||
|
|
|
||
|
|
// newRepo makes a temp directory that is unambiguously a repository root.
|
||
|
|
// Without the marker, resolution walks up into whatever repository happens to
|
||
|
|
// contain the temp directory -- this machine has a /tmp/.git, which is exactly
|
||
|
|
// the ambient surprise these tests must not depend on.
|
||
|
|
func newRepo(t *testing.T) string {
|
||
|
|
t.Helper()
|
||
|
|
dir := t.TempDir()
|
||
|
|
write(t, dir, ".git/HEAD", []byte("ref: refs/heads/main\n"))
|
||
|
|
return dir
|
||
|
|
}
|
||
|
|
|
||
|
|
func write(t *testing.T, dir, name string, data []byte) string {
|
||
|
|
t.Helper()
|
||
|
|
p := filepath.Join(dir, name)
|
||
|
|
os.MkdirAll(filepath.Dir(p), 0o755)
|
||
|
|
if err := os.WriteFile(p, data, 0o644); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
return p
|
||
|
|
}
|
||
|
|
|
||
|
|
// The schema documents the avatar path as repo-relative. Resolving it against
|
||
|
|
// the spec's own directory instead makes a correct spec fail, which is exactly
|
||
|
|
// what happened with the real campaign spec.
|
||
|
|
func TestPathIsRepoRelativeNotSpecRelative(t *testing.T) {
|
||
|
|
repo := newRepo(t)
|
||
|
|
write(t, repo, "assets/logo.png", png(t, 600, 600, 64))
|
||
|
|
specPath := write(t, repo, "presence/telegram.yaml", []byte("presence: {}\n"))
|
||
|
|
|
||
|
|
img, err := Load(specPath, "assets/logo.png")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("repo-relative path did not resolve: %v", err)
|
||
|
|
}
|
||
|
|
if img.Width != 600 || !img.Square() {
|
||
|
|
t.Errorf("got %dx%d", img.Width, img.Height)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMissingIsDistinguishable(t *testing.T) {
|
||
|
|
repo := newRepo(t)
|
||
|
|
spec := write(t, repo, "spec.yaml", []byte("x"))
|
||
|
|
_, err := Load(spec, "nope.png")
|
||
|
|
if !errors.Is(err, ErrMissing) {
|
||
|
|
t.Fatalf("a missing file must be distinguishable from a bad one: %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestRejectsBadImages(t *testing.T) {
|
||
|
|
repo := newRepo(t)
|
||
|
|
spec := write(t, repo, "spec.yaml", []byte("x"))
|
||
|
|
|
||
|
|
write(t, repo, "small.png", png(t, 128, 128, 16))
|
||
|
|
if _, err := Load(spec, "small.png"); err == nil {
|
||
|
|
t.Error("accepted an image below the minimum dimension")
|
||
|
|
} else if errors.Is(err, ErrMissing) {
|
||
|
|
t.Error("a too-small image is not a missing one")
|
||
|
|
}
|
||
|
|
|
||
|
|
write(t, repo, "notpng.png", []byte("GIF89a and then some padding bytes here"))
|
||
|
|
if _, err := Load(spec, "notpng.png"); err == nil {
|
||
|
|
t.Error("accepted a non-PNG")
|
||
|
|
}
|
||
|
|
|
||
|
|
write(t, repo, "zero.png", png(t, 0, 600, 16))
|
||
|
|
if _, err := Load(spec, "zero.png"); err == nil {
|
||
|
|
t.Error("accepted a zero dimension")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// The digest is what decides whether a picture is re-sent, so identical bytes
|
||
|
|
// must give an identical digest and different bytes must not.
|
||
|
|
func TestDigestIsContentAddressed(t *testing.T) {
|
||
|
|
repo := newRepo(t)
|
||
|
|
spec := write(t, repo, "spec.yaml", []byte("x"))
|
||
|
|
write(t, repo, "a.png", png(t, 600, 600, 32))
|
||
|
|
write(t, repo, "b.png", png(t, 600, 600, 32))
|
||
|
|
write(t, repo, "c.png", png(t, 600, 600, 33))
|
||
|
|
|
||
|
|
a, _ := Load(spec, "a.png")
|
||
|
|
b, _ := Load(spec, "b.png")
|
||
|
|
c, _ := Load(spec, "c.png")
|
||
|
|
if a.Digest != b.Digest {
|
||
|
|
t.Error("identical content gave different digests")
|
||
|
|
}
|
||
|
|
if a.Digest == c.Digest {
|
||
|
|
t.Error("different content gave the same digest")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// A non-square image is accepted but must say what a viewer will lose.
|
||
|
|
func TestCropNote(t *testing.T) {
|
||
|
|
repo := newRepo(t)
|
||
|
|
spec := write(t, repo, "spec.yaml", []byte("x"))
|
||
|
|
write(t, repo, "wide.png", png(t, 907, 885, 32))
|
||
|
|
img, err := Load(spec, "wide.png")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if img.Square() {
|
||
|
|
t.Fatal("907x885 is not square")
|
||
|
|
}
|
||
|
|
if note := img.CropNote(); note == "" {
|
||
|
|
t.Error("a non-square image should explain the crop")
|
||
|
|
}
|
||
|
|
|
||
|
|
write(t, repo, "sq.png", png(t, 900, 900, 32))
|
||
|
|
sq, _ := Load(spec, "sq.png")
|
||
|
|
if sq.CropNote() != "" {
|
||
|
|
t.Errorf("a square image needs no note, got %q", sq.CropNote())
|
||
|
|
}
|
||
|
|
}
|