| 1 | // SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | |
| 3 | package avatars_test |
| 4 | |
| 5 | import ( |
| 6 | "bytes" |
| 7 | "image" |
| 8 | "image/color" |
| 9 | "image/jpeg" |
| 10 | "image/png" |
| 11 | "strings" |
| 12 | "testing" |
| 13 | |
| 14 | "github.com/tenseleyFlow/shithub/internal/avatars" |
| 15 | ) |
| 16 | |
| 17 | func makePNG(t *testing.T, w, h int) []byte { |
| 18 | t.Helper() |
| 19 | img := image.NewRGBA(image.Rect(0, 0, w, h)) |
| 20 | for y := 0; y < h; y++ { |
| 21 | for x := 0; x < w; x++ { |
| 22 | img.Set(x, y, color.RGBA{R: 200, G: 100, B: 50, A: 255}) |
| 23 | } |
| 24 | } |
| 25 | buf := &bytes.Buffer{} |
| 26 | if err := png.Encode(buf, img); err != nil { |
| 27 | t.Fatalf("encode: %v", err) |
| 28 | } |
| 29 | return buf.Bytes() |
| 30 | } |
| 31 | |
| 32 | func makeJPEG(t *testing.T, w, h int) []byte { |
| 33 | t.Helper() |
| 34 | img := image.NewRGBA(image.Rect(0, 0, w, h)) |
| 35 | buf := &bytes.Buffer{} |
| 36 | if err := jpeg.Encode(buf, img, &jpeg.Options{Quality: 80}); err != nil { |
| 37 | t.Fatalf("encode: %v", err) |
| 38 | } |
| 39 | return buf.Bytes() |
| 40 | } |
| 41 | |
| 42 | func TestProcess_PNGRoundtrip(t *testing.T) { |
| 43 | t.Parallel() |
| 44 | src := makePNG(t, 800, 600) // landscape; expect center-crop to 600×600. |
| 45 | variants, hash, err := avatars.Process(bytes.NewReader(src)) |
| 46 | if err != nil { |
| 47 | t.Fatalf("process: %v", err) |
| 48 | } |
| 49 | if len(variants) != len(avatars.VariantSizes) { |
| 50 | t.Fatalf("variants = %d, want %d", len(variants), len(avatars.VariantSizes)) |
| 51 | } |
| 52 | for i, want := range avatars.VariantSizes { |
| 53 | v := variants[i] |
| 54 | if v.Size != want { |
| 55 | t.Errorf("variant[%d].Size = %d, want %d", i, v.Size, want) |
| 56 | } |
| 57 | cfg, err := png.DecodeConfig(bytes.NewReader(v.Data)) |
| 58 | if err != nil { |
| 59 | t.Errorf("variant[%d] decode: %v", i, err) |
| 60 | continue |
| 61 | } |
| 62 | if cfg.Width != want || cfg.Height != want { |
| 63 | t.Errorf("variant[%d] dims = %dx%d, want %dx%d", i, cfg.Width, cfg.Height, want, want) |
| 64 | } |
| 65 | } |
| 66 | if len(hash) == 0 { |
| 67 | t.Fatal("hash is empty") |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | func TestProcess_JPEGAccepted(t *testing.T) { |
| 72 | t.Parallel() |
| 73 | src := makeJPEG(t, 1000, 1000) |
| 74 | if _, _, err := avatars.Process(bytes.NewReader(src)); err != nil { |
| 75 | t.Fatalf("jpeg accepted: %v", err) |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | func TestProcess_RejectsUnsupportedFormat(t *testing.T) { |
| 80 | t.Parallel() |
| 81 | _, _, err := avatars.Process(strings.NewReader("not an image at all")) |
| 82 | if err == nil { |
| 83 | t.Fatal("expected error for non-image input") |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | func TestProcess_RejectsTooLarge(t *testing.T) { |
| 88 | t.Parallel() |
| 89 | // Build a payload that exceeds MaxUploadBytes. |
| 90 | big := make([]byte, avatars.MaxUploadBytes+10) |
| 91 | _, _, err := avatars.Process(bytes.NewReader(big)) |
| 92 | if err != avatars.ErrTooLarge { |
| 93 | t.Fatalf("err = %v, want ErrTooLarge", err) |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | func TestProcess_RejectsDecompressionBomb(t *testing.T) { |
| 98 | t.Parallel() |
| 99 | // Construct a tiny PNG header that *claims* huge dimensions but |
| 100 | // stays well under MaxUploadBytes. |
| 101 | // 10000 × 10000 = 100M pixels > 24M cap. |
| 102 | // Build a real but huge-size PNG using NewPaletted to keep file size low. |
| 103 | pal := []color.Color{color.White, color.Black} |
| 104 | img := image.NewPaletted(image.Rect(0, 0, 8000, 8000), pal) |
| 105 | buf := &bytes.Buffer{} |
| 106 | if err := png.Encode(buf, img); err != nil { |
| 107 | t.Fatalf("encode: %v", err) |
| 108 | } |
| 109 | if buf.Len() > avatars.MaxUploadBytes { |
| 110 | t.Skipf("constructed PNG (%d bytes) exceeds MaxUploadBytes; skipping", buf.Len()) |
| 111 | } |
| 112 | _, _, err := avatars.Process(bytes.NewReader(buf.Bytes())) |
| 113 | if err != avatars.ErrDecompression { |
| 114 | t.Fatalf("err = %v, want ErrDecompression", err) |
| 115 | } |
| 116 | } |
| 117 |