You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

175 lines
4.9 KiB

package image
import (
"bytes"
stdimage "image"
"image/color"
"image/jpeg"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/domagojzecevic/cammonitor/internal/config"
"github.com/domagojzecevic/cammonitor/internal/footage"
"github.com/go-chi/chi/v5"
)
func TestThumbnailEndpointReturnsSmallJPEG(t *testing.T) {
root := createImageFixture(t)
index := footage.NewIndex(root, 0)
t.Cleanup(index.Close)
router := imageRouter(root, index)
request := httptest.NewRequest(http.MethodGet, "/thumb/image/20260101/images/A26010112000001.jpg", nil)
response := httptest.NewRecorder()
router.ServeHTTP(response, request)
if response.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d: %s", http.StatusOK, response.Code, response.Body.String())
}
if contentType := response.Header().Get("Content-Type"); contentType != "image/jpeg" {
t.Fatalf("expected image/jpeg content type, got %q", contentType)
}
if response.Body.Len() > 30*1024 {
t.Fatalf("expected thumbnail <= 30 KB, got %d bytes", response.Body.Len())
}
thumbnail, err := jpeg.Decode(bytes.NewReader(response.Body.Bytes()))
if err != nil {
t.Fatalf("decode thumbnail: %v", err)
}
bounds := thumbnail.Bounds()
if bounds.Dx() > 160 || bounds.Dy() > 90 {
t.Fatalf("expected thumbnail within 160x90, got %dx%d", bounds.Dx(), bounds.Dy())
}
}
func TestRawEndpointServesOriginalFile(t *testing.T) {
root := createImageFixture(t)
originalPath := filepath.Join(root, "20260101", "images", "A26010112000001.jpg")
original, err := os.ReadFile(originalPath)
if err != nil {
t.Fatalf("read original: %v", err)
}
index := footage.NewIndex(root, 0)
t.Cleanup(index.Close)
router := imageRouter(root, index)
request := httptest.NewRequest(http.MethodGet, "/raw/image/20260101/images/A26010112000001.jpg", nil)
response := httptest.NewRecorder()
router.ServeHTTP(response, request)
if response.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d: %s", http.StatusOK, response.Code, response.Body.String())
}
served, err := io.ReadAll(response.Body)
if err != nil {
t.Fatalf("read response: %v", err)
}
if !bytes.Equal(served, original) {
t.Fatalf("expected raw endpoint to serve original bytes unchanged")
}
}
func TestRawEndpointRejectsTraversal(t *testing.T) {
root := createImageFixture(t)
index := footage.NewIndex(root, 0)
t.Cleanup(index.Close)
router := imageRouter(root, index)
request := httptest.NewRequest(http.MethodGet, "/raw/image/%2e%2e/secret.jpg", nil)
response := httptest.NewRecorder()
router.ServeHTTP(response, request)
if response.Code != http.StatusBadRequest {
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, response.Code)
}
}
func TestImagePageRendersViewerNavigation(t *testing.T) {
root := createImageFixture(t)
index := footage.NewIndex(root, 0)
t.Cleanup(index.Close)
router := imageRouter(root, index)
request := httptest.NewRequest(http.MethodGet, "/day/20260101/images?idx=1", nil)
response := httptest.NewRecorder()
router.ServeHTTP(response, request)
if response.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d: %s", http.StatusOK, response.Code, response.Body.String())
}
body := response.Body.String()
for _, want := range []string{
`data-image-browser`,
`data-active-index="1"`,
`/thumb/image/20260101/images/A26010112000001.jpg`,
`/raw/image/20260101/images/A26010112050001.jpg`,
`data-prev-index="0"`,
`data-next-index="0"`,
`keydown`,
`ArrowLeft`,
`ArrowRight`,
`ring-2 ring-indigo-400`,
} {
if !bytes.Contains(response.Body.Bytes(), []byte(want)) {
t.Fatalf("expected response to contain %q\nbody:\n%s", want, body)
}
}
}
func imageRouter(root string, index *footage.Index) chi.Router {
handler := NewHandler(&config.Config{FootageRoot: root}, index)
router := chi.NewRouter()
router.Get("/raw/image/*", handler.ServeRaw)
router.Get("/thumb/image/*", handler.ServeThumb)
router.Get("/day/{date}/images", handler.ServePage)
return router
}
func createImageFixture(t *testing.T) string {
t.Helper()
root := t.TempDir()
imageDir := filepath.Join(root, "20260101", "images")
if err := os.MkdirAll(imageDir, 0o755); err != nil {
t.Fatalf("create image dir: %v", err)
}
writeJPEG(t, filepath.Join(imageDir, "A26010112000001.jpg"), color.RGBA{R: 220, G: 40, B: 40, A: 255})
writeJPEG(t, filepath.Join(imageDir, "A26010112050001.jpg"), color.RGBA{R: 40, G: 90, B: 220, A: 255})
return root
}
func writeJPEG(t *testing.T, path string, fill color.Color) {
t.Helper()
img := stdimage.NewRGBA(stdimage.Rect(0, 0, 320, 180))
for y := 0; y < img.Bounds().Dy(); y++ {
for x := 0; x < img.Bounds().Dx(); x++ {
img.Set(x, y, fill)
}
}
file, err := os.Create(path)
if err != nil {
t.Fatalf("create jpeg %s: %v", path, err)
}
defer file.Close()
if err := jpeg.Encode(file, img, &jpeg.Options{Quality: 90}); err != nil {
t.Fatalf("encode jpeg %s: %v", path, err)
}
}