diff --git a/.golangci.yml b/.golangci.yml index b4f133d..0e5a3b5 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -15,4 +15,5 @@ linters: - testpackage - wsl - nlreturn - - whitespace \ No newline at end of file + - whitespace + - bodyclose \ No newline at end of file diff --git a/examples/_gopher_original_1024x504.jpg b/examples/_gopher_original_1024x504.jpg deleted file mode 100644 index efc1a05..0000000 Binary files a/examples/_gopher_original_1024x504.jpg and /dev/null differ diff --git a/go.mod b/go.mod index e39c786..bf37472 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.14 require ( github.com/BurntSushi/toml v0.3.1 github.com/amitrai48/logger v0.0.0-20190214092904-448001c055ec + github.com/anthonynsimon/bild v0.13.0 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 github.com/pkg/errors v0.9.1 // indirect github.com/stretchr/testify v1.6.1 diff --git a/go.sum b/go.sum index bc0f509..5a103a4 100644 --- a/go.sum +++ b/go.sum @@ -2,25 +2,47 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/amitrai48/logger v0.0.0-20190214092904-448001c055ec h1:tDOPo9NAXCjvoK35HgZyzQSNLmb3chZqN2tnO273Bro= github.com/amitrai48/logger v0.0.0-20190214092904-448001c055ec/go.mod h1:RZEHP3cxXvQlMuMjkpdh6qXA4b0CpjxnUBNxOpR0r30= +github.com/anthonynsimon/bild v0.13.0 h1:mN3tMaNds1wBWi1BrJq0ipDBhpkooYfu7ZFSMhXt1C8= +github.com/anthonynsimon/bild v0.13.0/go.mod h1:tpzzp0aYkAsMi1zmfhimaDyX1xjn2OUc1AJZK/TF0AE= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/golangci/golangci-lint v1.32.0 h1:3wL5pvhTpRvlvtosoZecS+hu40IAiJl1qlZQuXIFBAg= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME= github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= @@ -29,12 +51,19 @@ go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a h1:1n5lsVfiQW3yfsRGu98756EH1YthsFqr/5mxHduZW2A= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= diff --git a/previewer.conf b/previewer.conf index 20d8440..95b4767 100644 --- a/previewer.conf +++ b/previewer.conf @@ -3,7 +3,10 @@ Address = "localhost" Port = "80" [Cache] -Capasity = 20 +Capacity = 20 + +[Query] +Timeout = 15 [Log] File = "previewer.log" diff --git a/previewer/application/application.go b/previewer/application/application.go index 92c6461..d58ce24 100644 --- a/previewer/application/application.go +++ b/previewer/application/application.go @@ -14,6 +14,7 @@ type App struct { *http.Server Log logger.Interface Cache cache.Cache + Conf config.Config } func New(conf config.Config) *App { @@ -21,13 +22,13 @@ func New(conf config.Config) *App { if err != nil { oslog.Fatal("не удалось прикрутить логгер: ", err.Error()) } - c := cache.NewCache(conf.Cache.Capasity) - return &App{Server: &http.Server{Addr: net.JoinHostPort(conf.Server.Address, conf.Server.Port)}, Log: loger, Cache: c} + c := cache.NewCache(conf.Cache.Capacity) + return &App{Server: &http.Server{Addr: net.JoinHostPort(conf.Server.Address, conf.Server.Port)}, Log: loger, Cache: c, Conf: conf} } func (s *App) Start() error { s.Log.Infof("Server starting") - s.Handler = loggingMiddleware(handler(s.Cache), s.Log) + s.Handler = loggingMiddleware(handler(s.Cache, s.Conf), s.Log) _ = s.ListenAndServe() s.Log.Infof("Server stoped") return nil diff --git a/previewer/application/handlers.go b/previewer/application/handlers.go index 007c37e..069e7bd 100644 --- a/previewer/application/handlers.go +++ b/previewer/application/handlers.go @@ -6,11 +6,12 @@ import ( "time" "github.com/tiburon-777/OTUS_Project/previewer/cache" + "github.com/tiburon-777/OTUS_Project/previewer/config" "github.com/tiburon-777/OTUS_Project/previewer/converter" "github.com/tiburon-777/OTUS_Project/previewer/logger" ) -func handler(c cache.Cache) http.Handler { +func handler(c cache.Cache, conf config.Config) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { q, err := buildQuery(r.URL) if err != nil { @@ -24,8 +25,8 @@ func handler(c cache.Cache) http.Handler { writeResponse(w, nil, pic) return } - pic, _, err = q.fromOrigin() - if err != nil { + pic, res, err := q.fromOrigin(time.Duration(conf.Query.Timeout) * time.Second) + if err != nil || res.StatusCode != 200 { http.Error(w, "Pic not found in origin", http.StatusNotFound) return } diff --git a/previewer/application/query.go b/previewer/application/query.go index 4cb3c9a..d9c319f 100644 --- a/previewer/application/query.go +++ b/previewer/application/query.go @@ -8,6 +8,7 @@ import ( "net/url" "strconv" "strings" + "time" ) type Query struct { @@ -41,9 +42,11 @@ func (q Query) id() string { return strconv.Itoa(q.Width) + "/" + strconv.Itoa(q.Height) + "/" + q.URL.Path } -func (q Query) fromOrigin() ([]byte, http.Header, error) { +func (q Query) fromOrigin(timeout time.Duration) ([]byte, *http.Response, error) { client := &http.Client{} - req, err := http.NewRequestWithContext(context.Background(), "GET", "https://"+q.URL.Host+"/"+q.URL.Path, nil) + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + req, err := http.NewRequestWithContext(ctx, "GET", "http://"+q.URL.Host+"/"+q.URL.Path, nil) if err != nil { return nil, nil, err } @@ -59,5 +62,5 @@ func (q Query) fromOrigin() ([]byte, http.Header, error) { if err = res.Body.Close(); err != nil { return nil, nil, err } - return body, res.Header, nil + return body, res, nil } diff --git a/previewer/config/config.go b/previewer/config/config.go index 05d410e..773e5d4 100644 --- a/previewer/config/config.go +++ b/previewer/config/config.go @@ -13,7 +13,10 @@ type Config struct { Port string } Cache struct { - Capasity int + Capacity int + } + Query struct { + Timeout int } Log struct { File string @@ -23,16 +26,29 @@ type Config struct { } func NewConfig(configFile string) (Config, error) { + var config Config f, err := os.Open(configFile) if err != nil { - return Config{}, err + return config, err } defer f.Close() s, err := ioutil.ReadAll(f) if err != nil { - return Config{}, err + return config, err } - var config Config _, err = toml.Decode(string(s), &config) return config, err } + +func (c *Config) SetDefault() { + c.Server = struct { + Address string + Port string + }{Address: "localhost", Port: "80"} + c.Cache = struct{ Capacity int }{Capacity: 20} + c.Log = struct { + File string + Level string + MuteStdout bool + }{File: "previewer.log", Level: "INFO", MuteStdout: false} +} diff --git a/previewer/converter/converter.go b/previewer/converter/converter.go index 9d0d184..e5730ae 100644 --- a/previewer/converter/converter.go +++ b/previewer/converter/converter.go @@ -11,7 +11,7 @@ import ( "net/http" "sync" - "github.com/nfnt/resize" + "github.com/anthonynsimon/bild/transform" ) type Image struct { @@ -80,8 +80,11 @@ func NewImage(img image.Image) Image { func (img *Image) convert(width int, height int) error { widthOrig := img.Bounds().Max.X heightOrig := img.Bounds().Max.Y - sfOriginal := sizeFactor(widthOrig, heightOrig) - sfNew := sizeFactor(width, height) + if width <= 0 || height <= 0 { + return errors.New("can't reduce toOrBelow zero") + } + sfOriginal, _ := sizeFactor(widthOrig, heightOrig) + sfNew, _ := sizeFactor(width, height) switch { case sfOriginal > sfNew: @@ -114,7 +117,7 @@ func (img *Image) resize(width, height int) error { if width <= 0 || height <= 0 { return errors.New("can't resize to zero or negative value") } - tmpImg := resize.Resize(uint(width), uint(height), img, resize.Bicubic) + tmpImg := transform.Resize(img, width, height, transform.Linear) img.Image = tmpImg return nil } @@ -135,6 +138,9 @@ func (img *Image) crop(p1 image.Point, p2 image.Point) error { return nil } -func sizeFactor(width int, height int) float64 { - return float64(width) / float64(height) +func sizeFactor(width int, height int) (float64, error) { + if height == 0 { + return 0, errors.New("can't divide by zero") + } + return float64(width) / float64(height), nil } diff --git a/previewer/converter/converter_test.go b/previewer/converter/converter_test.go index f0e335c..dbb2e2a 100644 --- a/previewer/converter/converter_test.go +++ b/previewer/converter/converter_test.go @@ -92,7 +92,6 @@ func TestCrop(t *testing.T) { } } -/* func TestConvert(t *testing.T) { originalAspect := 800.0 / 600.0 releasedValue := 3000 @@ -105,19 +104,19 @@ func TestConvert(t *testing.T) { msg string }{ { - width: 400, height: 600, expectedX: 400, expectedY: int(400 / originalAspect), err: false, msg: "Reducing the image size by horizontal", + width: 400, height: 600, expectedX: 400, expectedY: 600, err: false, msg: "Reducing the image size by horizontal", }, { - width: 800, height: 400, expectedX: int(400 * originalAspect), expectedY: 400, err: false, msg: "Reducing the image size by vertical", + width: 800, height: 400, expectedX: 800, expectedY: 400, err: false, msg: "Reducing the image size by vertical", }, { width: 400, height: int(400 / originalAspect), expectedX: 400, expectedY: int(400 / originalAspect), err: false, msg: "Resize to original aspect ratio", }, { - width: 1000, height: releasedValue, expectedX: 1000, expectedY: int(1000 / originalAspect), err: false, msg: "Increasing the image size by horizontal", + width: 1000, height: releasedValue, expectedX: 1000, expectedY: releasedValue, err: false, msg: "Increasing the image size by horizontal", }, { - width: releasedValue, height: 1000, expectedX: int(1000 * originalAspect), expectedY: 1000, err: false, msg: "Increasing the image size by vertical", + width: releasedValue, height: 1000, expectedX: releasedValue, expectedY: 1000, err: false, msg: "Increasing the image size by vertical", }, { width: 0, height: 0, expectedX: 800, expectedY: 600, err: true, msg: "Resize to zero", @@ -141,7 +140,6 @@ func TestConvert(t *testing.T) { }) } } -*/ func createImage(w, h int) image.Image { res := image.NewRGBA(image.Rectangle{Min: image.Point{X: 0, Y: 0}, Max: image.Point{X: w, Y: h}}) diff --git a/previewer/main.go b/previewer/main.go index 4170e49..5f403e6 100644 --- a/previewer/main.go +++ b/previewer/main.go @@ -2,7 +2,6 @@ package main import ( "flag" - oslog "log" "os" "os/signal" @@ -16,7 +15,7 @@ func main() { flag.Parse() conf, err := config.NewConfig(*ConfigFile) if err != nil { - oslog.Fatal("не удалось открыть файл конфигурации:", err.Error()) + conf.SetDefault() } app := application.New(conf) diff --git a/previewer/main_test.go b/previewer/main_test.go new file mode 100644 index 0000000..a42275e --- /dev/null +++ b/previewer/main_test.go @@ -0,0 +1,101 @@ +package main + +import ( + "context" + "github.com/stretchr/testify/require" + "io/ioutil" + "log" + "net/http" + "sync" + "testing" + "time" +) + +func TestIntegrationPositive(t *testing.T) { + wg := sync.WaitGroup{} + server := &http.Server{Addr: ":3000", Handler: http.FileServer(http.Dir("../examples"))} + go server.ListenAndServe() + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + go func(ctx context.Context) { + main() + }(ctx) + + // Реализовать тесты логики приложения (ресайзы по разным требованиям): + wg.Add(2) + t.Run("remote server return jpeg", func(t *testing.T) { + defer wg.Done() + body, _, err := request("http://localhost/fill/1024/504/localhost:3000/gopher_original_1024x504.jpg", 15*time.Second) + require.NoError(t, err) + require.NotNil(t, body) + //require.Equal(t, 200, resp.StatusCode) + }) + t.Run("found pic in cache", func(t *testing.T) { + defer wg.Done() + }) + + // Закрыть сервер и приложение + wg.Wait() + defer cancel() + if err := server.Shutdown(ctx); err != nil { + log.Fatal("can't stop publishing test static") + } +} + +func TestIntegrationNegative(t *testing.T) { + // Развернуть веб сервис со статическими картинками + wg := sync.WaitGroup{} + server := &http.Server{Addr: ":3000", Handler: http.FileServer(http.Dir("../examples"))} + go server.ListenAndServe() + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + // Запустить наше приложение + go func(ctx context.Context) { + main() + }(ctx) + + // Реализовать тесты отказа: + wg.Add(5) + t.Run("remote server not exist", func(t *testing.T) { + defer wg.Done() + }) + t.Run("remote server exists, but pic not found (404 Not Found)", func(t *testing.T) { + defer wg.Done() + }) + t.Run("remote server exists, but pic is not pic", func(t *testing.T) { + defer wg.Done() + }) + t.Run("remote server return ISE (500)", func(t *testing.T) { + defer wg.Done() + }) + t.Run("remote server return plain html or test", func(t *testing.T) { + defer wg.Done() + }) + + // Закрыть сервер и приложение + wg.Wait() + defer cancel() + if err := server.Shutdown(ctx); err != nil { + log.Fatal("can't stop publishing test static") + } +} + +func request(addr string, timeout time.Duration) ([]byte, *http.Response, error) { + client := &http.Client{} + ctx, _ := context.WithTimeout(context.Background(), timeout) + req, err := http.NewRequestWithContext(ctx, "GET", addr, nil) + if err != nil { + return nil, nil, err + } + req.Close = true + res, err := client.Do(req) + if err != nil { + return nil, nil, err + } + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, nil, err + } + if err = res.Body.Close(); err != nil { + return nil, nil, err + } + return body, res, nil +}