From b2ecdb85beb7486a79959d2e75ca56e211a2ffeb Mon Sep 17 00:00:00 2001 From: Tomas Aparicio Date: Wed, 27 Jan 2016 16:20:25 +0000 Subject: [PATCH] feat: add more tests, partially document code --- .gitignore | 12 ++------- LICENSE | 2 +- health_test.go | 66 +++++++++++++++++++++++++++++++++++++++++++++++++ image.go | 49 +++++------------------------------- image_test.go | 22 +++++++++++++++++ log.go | 6 +++++ middleware.go | 23 ++++++++--------- options.go | 48 +++++++++++++++++++++++++++++++++++ options_test.go | 15 +++++++++++ 9 files changed, 178 insertions(+), 65 deletions(-) create mode 100644 health_test.go create mode 100644 image_test.go create mode 100644 options.go create mode 100644 options_test.go diff --git a/.gitignore b/.gitignore index 00c29d3..ddc9961 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ # Compiled Object files, Static and Dynamic libs (Shared Objects) -imgine *.o *.a *.so @@ -27,18 +26,11 @@ _testmain.go *.test bin/ .vagrant/ -website/build/ -website/npm-debug.log + *.old *.attr - -ui/.sass-cache -ui/static/base.css - -ui/static/application.min.js -ui/dist/ - *.swp + imaginary bin/imaginary diff --git a/LICENSE b/LICENSE index 51fe1d4..daa602a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License -Copyright (c) Tomas Aparicio and contributors +Copyright (c) 2015, 2016 Tomas Aparicio and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation diff --git a/health_test.go b/health_test.go new file mode 100644 index 0000000..1db2fbf --- /dev/null +++ b/health_test.go @@ -0,0 +1,66 @@ +package main + +import "testing" + +func TestToMegaBytes(t *testing.T) { + tests := []struct { + value uint64 + expected float64 + }{ + {1024, 0}, + {1024 * 1024, 1}, + {1024 * 1024 * 10, 10}, + {1024 * 1024 * 100, 100}, + {1024 * 1024 * 250, 250}, + } + + for _, test := range tests { + val := toMegaBytes(test.value) + if val != test.expected { + t.Errorf("Invalid param: %#v != %#v", val, test.expected) + } + } +} + +func TestRound(t *testing.T) { + tests := []struct { + value float64 + expected int + }{ + {0, 0}, + {1, 1}, + {1.56, 2}, + {1.38, 1}, + {30.12, 30}, + } + + for _, test := range tests { + val := round(test.value) + if val != test.expected { + t.Errorf("Invalid param: %#v != %#v", val, test.expected) + } + } +} + +func TestToFixed(t *testing.T) { + tests := []struct { + value float64 + expected float64 + }{ + {0, 0}, + {1, 1}, + {123, 123}, + {0.99, 1}, + {1.02, 1}, + {1.82, 1.8}, + {1.56, 1.6}, + {1.38, 1.4}, + } + + for _, test := range tests { + val := toFixed(test.value, 1) + if val != test.expected { + t.Errorf("Invalid param: %#v != %#v", val, test.expected) + } + } +} diff --git a/image.go b/image.go index 56fae2d..e6a9f85 100644 --- a/image.go +++ b/image.go @@ -6,60 +6,21 @@ import ( "gopkg.in/h2non/bimg.v0" ) -type ImageOptions struct { - Width int - Height int - AreaWidth int - AreaHeight int - Quality int - Compression int - Rotate int - Top int - Left int - Margin int - Factor int - DPI int - TextWidth int - Force bool - NoCrop bool - NoReplicate bool - NoRotation bool - NoProfile bool - Opacity float32 - Text string - Font string - Type string - Color []uint8 - Gravity bimg.Gravity - Colorspace bimg.Interpretation -} - +// Image stores an image binary buffer and its MIME type type Image struct { Body []byte Mime string } +// Operation implements an image transformation runnable interface type Operation func([]byte, ImageOptions) (Image, error) +// Run performs the image transformation func (o Operation) Run(buf []byte, opts ImageOptions) (Image, error) { return o(buf, opts) } -func BimgOptions(o ImageOptions) bimg.Options { - return bimg.Options{ - Width: o.Width, - Height: o.Height, - Quality: o.Quality, - Compression: o.Compression, - NoAutoRotate: o.NoRotation, - NoProfile: o.NoProfile, - Force: o.Force, - Gravity: o.Gravity, - Interpretation: o.Colorspace, - Type: ImageType(o.Type), - } -} - +// ImageInfo represents an image details and additional metadata type ImageInfo struct { Width int `json:"width"` Height int `json:"height"` @@ -72,6 +33,8 @@ type ImageInfo struct { } func Info(buf []byte, o ImageOptions) (Image, error) { + // We're not handling an image here, but we reused the struct. + // An interface will be definitively better here. image := Image{Mime: "application/json"} meta, err := bimg.Metadata(buf) diff --git a/image_test.go b/image_test.go new file mode 100644 index 0000000..1a56fd7 --- /dev/null +++ b/image_test.go @@ -0,0 +1,22 @@ +package main + +import ( + "io/ioutil" + "testing" +) + +func TestImageResize(t *testing.T) { + opts := ImageOptions{Width: 300, Height: 300} + buf, _ := ioutil.ReadAll(readFile("imaginary.jpg")) + + img, err := Resize(buf, opts) + if err != nil { + t.Errorf("Cannot process image: %s", err) + } + if img.Mime != "image/jpeg" { + t.Error("Invalid image MIME type") + } + if assertSize(img.Body, opts.Width, opts.Height) != nil { + t.Errorf("Invalid image size, expected: %dx%d", opts.Width, opts.Height) + } +} diff --git a/log.go b/log.go index 080cc3e..007e260 100644 --- a/log.go +++ b/log.go @@ -21,23 +21,28 @@ type LogRecord struct { elapsedTime time.Duration } +// Log writes a log entry in the passed io.Writer stream func (r *LogRecord) Log(out io.Writer) { timeFormat := r.time.Format("02/Jan/2006 03:04:05") request := fmt.Sprintf("%s %s %s", r.method, r.uri, r.protocol) fmt.Fprintf(out, formatPattern, r.ip, timeFormat, request, r.status, r.responseBytes, r.elapsedTime.Seconds()) } +// Write acts like a proxy passing the given bytes buffer to the ResponseWritter +// and additionally counting the passed amount of bytes for logging usage. func (r *LogRecord) Write(p []byte) (int, error) { written, err := r.ResponseWriter.Write(p) r.responseBytes += int64(written) return written, err } +// WriteHeader func (r *LogRecord) WriteHeader(status int) { r.status = status r.ResponseWriter.WriteHeader(status) } +// LogHandler maps the HTTP handler with a custom io.Writer compatible stream type LogHandler struct { handler http.Handler io io.Writer @@ -48,6 +53,7 @@ func NewLog(handler http.Handler, io io.Writer) http.Handler { return &LogHandler{handler, io} } +// Implementes the required method as standard HTTP handler, serving the request. func (h *LogHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { clientIP := r.RemoteAddr if colon := strings.LastIndex(clientIP, ":"); colon != -1 { diff --git a/middleware.go b/middleware.go index d8132ce..51cfbfc 100644 --- a/middleware.go +++ b/middleware.go @@ -79,7 +79,7 @@ func validate(next http.Handler) http.Handler { func validateImage(next http.Handler, o ServerOptions) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { path := r.URL.Path - if r.Method == "GET" && isPrivatePath(path) { + if r.Method == "GET" && isPublicPath(path) { next.ServeHTTP(w, r) return } @@ -120,24 +120,25 @@ func setCacheHeaders(next http.Handler, ttl int) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer next.ServeHTTP(w, r) - if r.Method != "GET" || isPrivatePath(r.URL.Path) { + if r.Method != "GET" || isPublicPath(r.URL.Path) { return } - var cacheControl string - if ttl == 0 { - cacheControl = "private, no-cache, no-store, must-revalidate" - } else { - cacheControl = fmt.Sprintf("public, s-maxage: %d, max-age: %d, no-transform", ttl, ttl) - } - ttlDiff := time.Duration(ttl) * time.Second expires := time.Now().Add(ttlDiff) + w.Header().Add("Expires", expires.Format(time.RFC1123)) - w.Header().Add("Cache-Control", cacheControl) + w.Header().Add("Cache-Control", getCacheControl(ttl)) }) } -func isPrivatePath(path string) bool { +func getCacheControl(ttl int) string { + if ttl == 0 { + return "private, no-cache, no-store, must-revalidate" + } + return fmt.Sprintf("public, s-maxage: %d, max-age: %d, no-transform", ttl, ttl) +} + +func isPublicPath(path string) bool { return path == "/" || path == "/health" || path == "/form" } diff --git a/options.go b/options.go new file mode 100644 index 0000000..0ec7409 --- /dev/null +++ b/options.go @@ -0,0 +1,48 @@ +package main + +import "gopkg.in/h2non/bimg.v0" + +// ImageOptions represent all the supported image transformation params as first level members +type ImageOptions struct { + Width int + Height int + AreaWidth int + AreaHeight int + Quality int + Compression int + Rotate int + Top int + Left int + Margin int + Factor int + DPI int + TextWidth int + Force bool + NoCrop bool + NoReplicate bool + NoRotation bool + NoProfile bool + Opacity float32 + Text string + Font string + Type string + Color []uint8 + Gravity bimg.Gravity + Colorspace bimg.Interpretation +} + +// BimgOptions creates a new bimg compatible options struct mapping the fields properly +func BimgOptions(o ImageOptions) bimg.Options { + return bimg.Options{ + Width: o.Width, + Height: o.Height, + Quality: o.Quality, + Compression: o.Compression, + NoAutoRotate: o.NoRotation, + NoProfile: o.NoProfile, + Force: o.Force, + Gravity: o.Gravity, + Interpretation: o.Colorspace, + Type: ImageType(o.Type), + } +} diff --git a/options_test.go b/options_test.go new file mode 100644 index 0000000..ecc89b1 --- /dev/null +++ b/options_test.go @@ -0,0 +1,15 @@ +package main + +import "testing" + +func TestBimgOptions(t *testing.T) { + imgOpts := ImageOptions{ + Width: 500, + Height: 600, + } + opts := BimgOptions(imgOpts) + + if opts.Width != imgOpts.Width || opts.Height != imgOpts.Height { + t.Error("Invalid width and height") + } +}