feat: add more tests, partially document code
This commit is contained in:
parent
9cb131c037
commit
b2ecdb85be
|
@ -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
|
||||
|
||||
|
|
2
LICENSE
2
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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
49
image.go
49
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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
6
log.go
6
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 {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue