Merge branch 'forgejo' into forgejo
This commit is contained in:
commit
75f703326f
12 changed files with 892 additions and 32 deletions
323
modules/card/card.go
Normal file
323
modules/card/card.go
Normal file
|
@ -0,0 +1,323 @@
|
|||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package card
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/color"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
_ "image/gif" // for processing gif images
|
||||
_ "image/jpeg" // for processing jpeg images
|
||||
_ "image/png" // for processing png images
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/proxy"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/golang/freetype"
|
||||
"github.com/golang/freetype/truetype"
|
||||
"golang.org/x/image/draw"
|
||||
"golang.org/x/image/font"
|
||||
"golang.org/x/image/font/gofont/goregular"
|
||||
|
||||
_ "golang.org/x/image/webp" // for processing webp images
|
||||
)
|
||||
|
||||
type Card struct {
|
||||
Img *image.RGBA
|
||||
Font *truetype.Font
|
||||
Margin int
|
||||
}
|
||||
|
||||
var fontCache = sync.OnceValues(func() (*truetype.Font, error) {
|
||||
return truetype.Parse(goregular.TTF)
|
||||
})
|
||||
|
||||
// NewCard creates a new card with the given dimensions in pixels
|
||||
func NewCard(width, height int) (*Card, error) {
|
||||
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
draw.Draw(img, img.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src)
|
||||
|
||||
font, err := fontCache()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Card{
|
||||
Img: img,
|
||||
Font: font,
|
||||
Margin: 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Split splits the card horizontally or vertically by a given percentage; the first card returned has the percentage
|
||||
// size, and the second card has the remainder. Both cards draw to a subsection of the same image buffer.
|
||||
func (c *Card) Split(vertical bool, percentage int) (*Card, *Card) {
|
||||
bounds := c.Img.Bounds()
|
||||
bounds = image.Rect(bounds.Min.X+c.Margin, bounds.Min.Y+c.Margin, bounds.Max.X-c.Margin, bounds.Max.Y-c.Margin)
|
||||
if vertical {
|
||||
mid := (bounds.Dx() * percentage / 100) + bounds.Min.X
|
||||
subleft := c.Img.SubImage(image.Rect(bounds.Min.X, bounds.Min.Y, mid, bounds.Max.Y)).(*image.RGBA)
|
||||
subright := c.Img.SubImage(image.Rect(mid, bounds.Min.Y, bounds.Max.X, bounds.Max.Y)).(*image.RGBA)
|
||||
return &Card{Img: subleft, Font: c.Font},
|
||||
&Card{Img: subright, Font: c.Font}
|
||||
}
|
||||
mid := (bounds.Dy() * percentage / 100) + bounds.Min.Y
|
||||
subtop := c.Img.SubImage(image.Rect(bounds.Min.X, bounds.Min.Y, bounds.Max.X, mid)).(*image.RGBA)
|
||||
subbottom := c.Img.SubImage(image.Rect(bounds.Min.X, mid, bounds.Max.X, bounds.Max.Y)).(*image.RGBA)
|
||||
return &Card{Img: subtop, Font: c.Font},
|
||||
&Card{Img: subbottom, Font: c.Font}
|
||||
}
|
||||
|
||||
// SetMargin sets the margins for the card
|
||||
func (c *Card) SetMargin(margin int) {
|
||||
c.Margin = margin
|
||||
}
|
||||
|
||||
type (
|
||||
VAlign int64
|
||||
HAlign int64
|
||||
)
|
||||
|
||||
const (
|
||||
Top VAlign = iota
|
||||
Middle
|
||||
Bottom
|
||||
)
|
||||
|
||||
const (
|
||||
Left HAlign = iota
|
||||
Center
|
||||
Right
|
||||
)
|
||||
|
||||
// DrawText draws text within the card, respecting margins and alignment
|
||||
func (c *Card) DrawText(text string, textColor color.Color, sizePt float64, valign VAlign, halign HAlign) ([]string, error) {
|
||||
ft := freetype.NewContext()
|
||||
ft.SetDPI(72)
|
||||
ft.SetFont(c.Font)
|
||||
ft.SetFontSize(sizePt)
|
||||
ft.SetClip(c.Img.Bounds())
|
||||
ft.SetDst(c.Img)
|
||||
ft.SetSrc(image.NewUniform(textColor))
|
||||
|
||||
face := truetype.NewFace(c.Font, &truetype.Options{Size: sizePt, DPI: 72})
|
||||
fontHeight := ft.PointToFixed(sizePt).Ceil()
|
||||
|
||||
bounds := c.Img.Bounds()
|
||||
bounds = image.Rect(bounds.Min.X+c.Margin, bounds.Min.Y+c.Margin, bounds.Max.X-c.Margin, bounds.Max.Y-c.Margin)
|
||||
boxWidth, boxHeight := bounds.Size().X, bounds.Size().Y
|
||||
// draw.Draw(c.Img, bounds, image.NewUniform(color.Gray{128}), image.Point{}, draw.Src) // Debug draw box
|
||||
|
||||
// Try to apply wrapping to this text; we'll find the most text that will fit into one line, record that line, move
|
||||
// on. We precalculate each line before drawing so that we can support valign="middle" correctly which requires
|
||||
// knowing the total height, which is related to how many lines we'll have.
|
||||
lines := make([]string, 0)
|
||||
textWords := strings.Split(text, " ")
|
||||
currentLine := ""
|
||||
heightTotal := 0
|
||||
|
||||
for {
|
||||
if len(textWords) == 0 {
|
||||
// Ran out of words.
|
||||
if currentLine != "" {
|
||||
heightTotal += fontHeight
|
||||
lines = append(lines, currentLine)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
nextWord := textWords[0]
|
||||
proposedLine := currentLine
|
||||
if proposedLine != "" {
|
||||
proposedLine += " "
|
||||
}
|
||||
proposedLine += nextWord
|
||||
|
||||
proposedLineWidth := font.MeasureString(face, proposedLine)
|
||||
if proposedLineWidth.Ceil() > boxWidth {
|
||||
// no, proposed line is too big; we'll use the last "currentLine"
|
||||
heightTotal += fontHeight
|
||||
if currentLine != "" {
|
||||
lines = append(lines, currentLine)
|
||||
currentLine = ""
|
||||
// leave nextWord in textWords and keep going
|
||||
} else {
|
||||
// just nextWord by itself doesn't fit on a line; well, we can't skip it, but we'll consume it
|
||||
// regardless as a line by itself. It will be clipped by the drawing routine.
|
||||
lines = append(lines, nextWord)
|
||||
textWords = textWords[1:]
|
||||
}
|
||||
} else {
|
||||
// yes, it will fit
|
||||
currentLine = proposedLine
|
||||
textWords = textWords[1:]
|
||||
}
|
||||
}
|
||||
|
||||
textY := 0
|
||||
switch valign {
|
||||
case Top:
|
||||
textY = fontHeight
|
||||
case Bottom:
|
||||
textY = boxHeight - heightTotal + fontHeight
|
||||
case Middle:
|
||||
textY = ((boxHeight - heightTotal) / 2) + fontHeight
|
||||
}
|
||||
|
||||
for _, line := range lines {
|
||||
lineWidth := font.MeasureString(face, line)
|
||||
|
||||
textX := 0
|
||||
switch halign {
|
||||
case Left:
|
||||
textX = 0
|
||||
case Right:
|
||||
textX = boxWidth - lineWidth.Ceil()
|
||||
case Center:
|
||||
textX = (boxWidth - lineWidth.Ceil()) / 2
|
||||
}
|
||||
|
||||
pt := freetype.Pt(bounds.Min.X+textX, bounds.Min.Y+textY)
|
||||
_, err := ft.DrawString(line, pt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
textY += fontHeight
|
||||
}
|
||||
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
// DrawImage fills the card with an image, scaled to maintain the original aspect ratio and centered with respect to the non-filled dimension
|
||||
func (c *Card) DrawImage(img image.Image) {
|
||||
bounds := c.Img.Bounds()
|
||||
targetRect := image.Rect(bounds.Min.X+c.Margin, bounds.Min.Y+c.Margin, bounds.Max.X-c.Margin, bounds.Max.Y-c.Margin)
|
||||
srcBounds := img.Bounds()
|
||||
srcAspect := float64(srcBounds.Dx()) / float64(srcBounds.Dy())
|
||||
targetAspect := float64(targetRect.Dx()) / float64(targetRect.Dy())
|
||||
|
||||
var scale float64
|
||||
if srcAspect > targetAspect {
|
||||
// Image is wider than target, scale by width
|
||||
scale = float64(targetRect.Dx()) / float64(srcBounds.Dx())
|
||||
} else {
|
||||
// Image is taller or equal, scale by height
|
||||
scale = float64(targetRect.Dy()) / float64(srcBounds.Dy())
|
||||
}
|
||||
|
||||
newWidth := int(math.Round(float64(srcBounds.Dx()) * scale))
|
||||
newHeight := int(math.Round(float64(srcBounds.Dy()) * scale))
|
||||
|
||||
// Center the image within the target rectangle
|
||||
offsetX := (targetRect.Dx() - newWidth) / 2
|
||||
offsetY := (targetRect.Dy() - newHeight) / 2
|
||||
|
||||
scaledRect := image.Rect(targetRect.Min.X+offsetX, targetRect.Min.Y+offsetY, targetRect.Min.X+offsetX+newWidth, targetRect.Min.Y+offsetY+newHeight)
|
||||
draw.CatmullRom.Scale(c.Img, scaledRect, img, srcBounds, draw.Over, nil)
|
||||
}
|
||||
|
||||
func fallbackImage() image.Image {
|
||||
// can't usage image.Uniform(color.White) because it's infinitely sized causing a panic in the scaler in DrawImage
|
||||
img := image.NewRGBA(image.Rect(0, 0, 1, 1))
|
||||
img.Set(0, 0, color.White)
|
||||
return img
|
||||
}
|
||||
|
||||
// As defensively as possible, attempt to load an image from a presumed external and untrusted URL
|
||||
func (c *Card) fetchExternalImage(url string) (image.Image, bool) {
|
||||
// Use a short timeout; in the event of any failure we'll be logging and returning a placeholder, but we don't want
|
||||
// this rendering process to be slowed down
|
||||
client := &http.Client{
|
||||
Timeout: 1 * time.Second, // 1 second timeout
|
||||
Transport: &http.Transport{
|
||||
Proxy: proxy.Proxy(),
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
log.Warn("error when fetching external image from %s: %w", url, err)
|
||||
return nil, false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Warn("non-OK error code when fetching external image from %s: %s", url, resp.Status)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
// Support content types are in-sync with the allowed custom avatar file types
|
||||
if contentType != "image/png" && contentType != "image/jpeg" && contentType != "image/gif" && contentType != "image/webp" {
|
||||
log.Warn("fetching external image returned unsupported Content-Type which was ignored: %s", contentType)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
body := io.LimitReader(resp.Body, setting.Avatar.MaxFileSize)
|
||||
bodyBytes, err := io.ReadAll(body)
|
||||
if err != nil {
|
||||
log.Warn("error when fetching external image from %s: %w", url, err)
|
||||
return nil, false
|
||||
}
|
||||
if int64(len(bodyBytes)) == setting.Avatar.MaxFileSize {
|
||||
log.Warn("while fetching external image response size hit MaxFileSize (%d) and was discarded from url %s", setting.Avatar.MaxFileSize, url)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
bodyBuffer := bytes.NewReader(bodyBytes)
|
||||
imgCfg, imgType, err := image.DecodeConfig(bodyBuffer)
|
||||
if err != nil {
|
||||
log.Warn("error when decoding external image from %s: %w", url, err)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Verify that we have a match between actual data understood in the image body and the reported Content-Type
|
||||
if (contentType == "image/png" && imgType != "png") ||
|
||||
(contentType == "image/jpeg" && imgType != "jpeg") ||
|
||||
(contentType == "image/gif" && imgType != "gif") ||
|
||||
(contentType == "image/webp" && imgType != "webp") {
|
||||
log.Warn("while fetching external image, mismatched image body (%s) and Content-Type (%s)", imgType, contentType)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// do not process image which is too large, it would consume too much memory
|
||||
if imgCfg.Width > setting.Avatar.MaxWidth {
|
||||
log.Warn("while fetching external image, width %d exceeds Avatar.MaxWidth %d", imgCfg.Width, setting.Avatar.MaxWidth)
|
||||
return nil, false
|
||||
}
|
||||
if imgCfg.Height > setting.Avatar.MaxHeight {
|
||||
log.Warn("while fetching external image, height %d exceeds Avatar.MaxHeight %d", imgCfg.Height, setting.Avatar.MaxHeight)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
_, err = bodyBuffer.Seek(0, io.SeekStart) // reset for actual decode
|
||||
if err != nil {
|
||||
log.Warn("error w/ bodyBuffer.Seek")
|
||||
return nil, false
|
||||
}
|
||||
img, _, err := image.Decode(bodyBuffer)
|
||||
if err != nil {
|
||||
log.Warn("error when decoding external image from %s: %w", url, err)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return img, true
|
||||
}
|
||||
|
||||
func (c *Card) DrawExternalImage(url string) {
|
||||
image, ok := c.fetchExternalImage(url)
|
||||
if !ok {
|
||||
image = fallbackImage()
|
||||
}
|
||||
c.DrawImage(image)
|
||||
}
|
244
modules/card/card_test.go
Normal file
244
modules/card/card_test.go
Normal file
|
@ -0,0 +1,244 @@
|
|||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package card
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/golang/freetype/truetype"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/image/font/gofont/goregular"
|
||||
)
|
||||
|
||||
func TestNewCard(t *testing.T) {
|
||||
width, height := 100, 50
|
||||
card, err := NewCard(width, height)
|
||||
require.NoError(t, err, "No error should occur when creating a new card")
|
||||
assert.NotNil(t, card, "Card should not be nil")
|
||||
assert.Equal(t, width, card.Img.Bounds().Dx(), "Width should match the provided width")
|
||||
assert.Equal(t, height, card.Img.Bounds().Dy(), "Height should match the provided height")
|
||||
|
||||
// Checking default margin
|
||||
assert.Equal(t, 0, card.Margin, "Default margin should be 0")
|
||||
|
||||
// Checking font parsing
|
||||
originalFont, _ := truetype.Parse(goregular.TTF)
|
||||
assert.Equal(t, originalFont, card.Font, "Fonts should be equivalent")
|
||||
}
|
||||
|
||||
func TestSplit(t *testing.T) {
|
||||
// Note: you normally wouldn't split the same card twice as draw operations would start to overlap each other; but
|
||||
// it's fine for this limited scope test
|
||||
card, _ := NewCard(200, 100)
|
||||
|
||||
// Test vertical split
|
||||
leftCard, rightCard := card.Split(true, 50)
|
||||
assert.Equal(t, 100, leftCard.Img.Bounds().Dx(), "Left card should have half the width of original")
|
||||
assert.Equal(t, 100, leftCard.Img.Bounds().Dy(), "Left card height unchanged by split")
|
||||
assert.Equal(t, 100, rightCard.Img.Bounds().Dx(), "Right card should have half the width of original")
|
||||
assert.Equal(t, 100, rightCard.Img.Bounds().Dy(), "Right card height unchanged by split")
|
||||
|
||||
// Test horizontal split
|
||||
topCard, bottomCard := card.Split(false, 50)
|
||||
assert.Equal(t, 200, topCard.Img.Bounds().Dx(), "Top card width unchanged by split")
|
||||
assert.Equal(t, 50, topCard.Img.Bounds().Dy(), "Top card should have half the height of original")
|
||||
assert.Equal(t, 200, bottomCard.Img.Bounds().Dx(), "Bottom width unchanged by split")
|
||||
assert.Equal(t, 50, bottomCard.Img.Bounds().Dy(), "Bottom card should have half the height of original")
|
||||
}
|
||||
|
||||
func TestDrawTextSingleLine(t *testing.T) {
|
||||
card, _ := NewCard(300, 100)
|
||||
lines, err := card.DrawText("This is a single line", color.Black, 12, Middle, Center)
|
||||
require.NoError(t, err, "No error should occur when drawing text")
|
||||
assert.Len(t, lines, 1, "Should be exactly one line")
|
||||
assert.Equal(t, "This is a single line", lines[0], "Text should match the input")
|
||||
}
|
||||
|
||||
func TestDrawTextLongLine(t *testing.T) {
|
||||
card, _ := NewCard(300, 100)
|
||||
text := "This text is definitely too long to fit in three hundred pixels width without wrapping"
|
||||
lines, err := card.DrawText(text, color.Black, 12, Middle, Center)
|
||||
require.NoError(t, err, "No error should occur when drawing text")
|
||||
assert.Len(t, lines, 2, "Text should wrap into multiple lines")
|
||||
assert.Equal(t, "This text is definitely too long to fit in three hundred", lines[0], "Text should match the input")
|
||||
assert.Equal(t, "pixels width without wrapping", lines[1], "Text should match the input")
|
||||
}
|
||||
|
||||
func TestDrawTextWordTooLong(t *testing.T) {
|
||||
card, _ := NewCard(300, 100)
|
||||
text := "Line 1 Superduperlongwordthatcannotbewrappedbutshouldenduponitsownsingleline Line 3"
|
||||
lines, err := card.DrawText(text, color.Black, 12, Middle, Center)
|
||||
require.NoError(t, err, "No error should occur when drawing text")
|
||||
assert.Len(t, lines, 3, "Text should create two lines despite long word")
|
||||
assert.Equal(t, "Line 1", lines[0], "First line should contain text before the long word")
|
||||
assert.Equal(t, "Superduperlongwordthatcannotbewrappedbutshouldenduponitsownsingleline", lines[1], "Second line couldn't wrap the word so it just overflowed")
|
||||
assert.Equal(t, "Line 3", lines[2], "Third line continued with wrapping")
|
||||
}
|
||||
|
||||
func TestFetchExternalImageServer(t *testing.T) {
|
||||
blackPng, err := base64.URLEncoding.DecodeString("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAACklEQVR4AWNgAAAAAgABc3UBGAAAAABJRU5ErkJggg==")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
var tooWideBuf bytes.Buffer
|
||||
imgTooWide := image.NewGray(image.Rect(0, 0, 16001, 10))
|
||||
err = png.Encode(&tooWideBuf, imgTooWide)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
imgTooWidePng := tooWideBuf.Bytes()
|
||||
|
||||
var tooTallBuf bytes.Buffer
|
||||
imgTooTall := image.NewGray(image.Rect(0, 0, 10, 16002))
|
||||
err = png.Encode(&tooTallBuf, imgTooTall)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
imgTooTallPng := tooTallBuf.Bytes()
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/timeout":
|
||||
// Simulate a timeout by taking a long time to respond
|
||||
time.Sleep(8 * time.Second)
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
w.Write(blackPng)
|
||||
case "/notfound":
|
||||
http.NotFound(w, r)
|
||||
case "/image.png":
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
w.Write(blackPng)
|
||||
case "/weird-content":
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte("<html></html>"))
|
||||
case "/giant-response":
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
w.Write(make([]byte, 10485760))
|
||||
case "/invalid.png":
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
w.Write(make([]byte, 100))
|
||||
case "/mismatched.jpg":
|
||||
w.Header().Set("Content-Type", "image/jpeg")
|
||||
w.Write(blackPng) // valid png, but wrong content-type
|
||||
case "/too-wide.png":
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
w.Write(imgTooWidePng)
|
||||
case "/too-tall.png":
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
w.Write(imgTooTallPng)
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
expectedSuccess bool
|
||||
expectedLog string
|
||||
}{
|
||||
{
|
||||
name: "timeout error",
|
||||
url: "/timeout",
|
||||
expectedSuccess: false,
|
||||
expectedLog: "error when fetching external image from",
|
||||
},
|
||||
{
|
||||
name: "external fetch success",
|
||||
url: "/image.png",
|
||||
expectedSuccess: true,
|
||||
expectedLog: "",
|
||||
},
|
||||
{
|
||||
name: "404 fallback",
|
||||
url: "/notfound",
|
||||
expectedSuccess: false,
|
||||
expectedLog: "non-OK error code when fetching external image",
|
||||
},
|
||||
{
|
||||
name: "unsupported content type",
|
||||
url: "/weird-content",
|
||||
expectedSuccess: false,
|
||||
expectedLog: "fetching external image returned unsupported Content-Type",
|
||||
},
|
||||
{
|
||||
name: "response too large",
|
||||
url: "/giant-response",
|
||||
expectedSuccess: false,
|
||||
expectedLog: "while fetching external image response size hit MaxFileSize",
|
||||
},
|
||||
{
|
||||
name: "invalid png",
|
||||
url: "/invalid.png",
|
||||
expectedSuccess: false,
|
||||
expectedLog: "error when decoding external image",
|
||||
},
|
||||
{
|
||||
name: "mismatched content type",
|
||||
url: "/mismatched.jpg",
|
||||
expectedSuccess: false,
|
||||
expectedLog: "while fetching external image, mismatched image body",
|
||||
},
|
||||
{
|
||||
name: "too wide",
|
||||
url: "/too-wide.png",
|
||||
expectedSuccess: false,
|
||||
expectedLog: "while fetching external image, width 16001 exceeds Avatar.MaxWidth",
|
||||
},
|
||||
{
|
||||
name: "too tall",
|
||||
url: "/too-tall.png",
|
||||
expectedSuccess: false,
|
||||
expectedLog: "while fetching external image, height 16002 exceeds Avatar.MaxHeight",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range tests {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
// stopMark is used as a logging boundary to verify that the expected message (testCase.expectedLog) is
|
||||
// logged during the `fetchExternalImage` operation. This is verified by a combination of checking that the
|
||||
// stopMark message was received, and that the filtered log (logFiltered[0]) was received.
|
||||
stopMark := fmt.Sprintf(">>>>>>>>>>>>>STOP: %s<<<<<<<<<<<<<<<", testCase.name)
|
||||
|
||||
logChecker, cleanup := test.NewLogChecker(log.DEFAULT, log.TRACE)
|
||||
logChecker.Filter(testCase.expectedLog).StopMark(stopMark)
|
||||
defer cleanup()
|
||||
|
||||
card, _ := NewCard(100, 100)
|
||||
img, ok := card.fetchExternalImage(server.URL + testCase.url)
|
||||
|
||||
if testCase.expectedSuccess {
|
||||
assert.True(t, ok, "expected success from fetchExternalImage")
|
||||
assert.NotNil(t, img)
|
||||
} else {
|
||||
assert.False(t, ok, "expected failure from fetchExternalImage")
|
||||
assert.Nil(t, img)
|
||||
}
|
||||
|
||||
log.Info(stopMark)
|
||||
|
||||
logFiltered, logStopped := logChecker.Check(5 * time.Second)
|
||||
assert.True(t, logStopped, "failed to find log stop mark")
|
||||
assert.True(t, logFiltered[0], "failed to find in log: '%s'", testCase.expectedLog)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue