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
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)
|
|
}
|
|
}
|
|
|