From d41703227c966bab47c0e4ceeacc15d8b7e6035d Mon Sep 17 00:00:00 2001 From: Pablo Lalloni Date: Mon, 25 Jun 2018 01:38:11 -0300 Subject: [PATCH] fix: add internal/http tests --- internal/http/http.go | 103 +++++++++------ internal/http/http_test.go | 252 +++++++++++++++++++++++++++++++++++++ 2 files changed, 319 insertions(+), 36 deletions(-) create mode 100644 internal/http/http_test.go diff --git a/internal/http/http.go b/internal/http/http.go index c4d087f3b..77a1bb4e8 100644 --- a/internal/http/http.go +++ b/internal/http/http.go @@ -28,6 +28,41 @@ const ( ModeArchive = "archive" ) +type asset struct { + ReadCloser io.ReadCloser + Size int64 +} + +type assetOpenFunc func(string, *artifact.Artifact) (*asset, error) + +var assetOpen assetOpenFunc + +func init() { + assetOpenReset() +} + +func assetOpenReset() { + assetOpen = assetOpenDefault +} + +func assetOpenDefault(kind string, a *artifact.Artifact) (*asset, error) { + f, err := os.Open(a.Path) + if err != nil { + return nil, err + } + s, err := f.Stat() + if err != nil { + return nil, err + } + if s.IsDir() { + return nil, errors.Errorf("%s: upload failed: the asset to upload can't be a directory", kind) + } + return &asset{ + ReadCloser: f, + Size: s.Size(), + }, nil +} + // Defaults sets default configuration options on Put structs func Defaults(puts []config.Put) error { for i := range puts { @@ -42,24 +77,28 @@ func defaults(put *config.Put) { } } -// CheckConfig validates an HTTPUpload configuration returning a descriptive error when appropriate -func CheckConfig(ctx *context.Context, upload *config.Put, kind string) error { +// CheckConfig validates a Put configuration returning a descriptive error when appropriate +func CheckConfig(ctx *context.Context, put *config.Put, kind string) error { - if upload.Target == "" { - return misconfigured(kind, upload, "missing target") + if put.Target == "" { + return misconfigured(kind, put, "missing target") } - if upload.Username == "" { - return misconfigured(kind, upload, "missing username") + if put.Username == "" { + return misconfigured(kind, put, "missing username") } - if upload.Name == "" { - return misconfigured(kind, upload, "missing name") + if put.Name == "" { + return misconfigured(kind, put, "missing name") } - envName := fmt.Sprintf("%s_%s_SECRET", strings.ToUpper(kind), strings.ToUpper(upload.Name)) + if put.Mode != ModeArchive && put.Mode != ModeBinary { + return misconfigured(kind, put, "mode must be 'binary' or 'archive'") + } + + envName := fmt.Sprintf("%s_%s_SECRET", strings.ToUpper(kind), strings.ToUpper(put.Name)) if _, ok := ctx.Env[envName]; !ok { - return misconfigured(kind, upload, fmt.Sprintf("missing %s environment variable", envName)) + return misconfigured(kind, put, fmt.Sprintf("missing %s environment variable", envName)) } return nil @@ -117,7 +156,7 @@ func Upload(ctx *context.Context, puts []config.Put, kind string, check Response return nil } -func runPipeByFilter(ctx *context.Context, instance config.Put, filter artifact.Filter, kind string, check ResponseChecker) error { +func runPipeByFilter(ctx *context.Context, put config.Put, filter artifact.Filter, kind string, check ResponseChecker) error { sem := make(chan bool, ctx.Parallelism) var g errgroup.Group for _, artifact := range ctx.Artifacts.Filter(filter).List() { @@ -127,31 +166,31 @@ func runPipeByFilter(ctx *context.Context, instance config.Put, filter artifact. defer func() { <-sem }() - return uploadAsset(ctx, instance, artifact, kind, check) + return uploadAsset(ctx, put, artifact, kind, check) }) } return g.Wait() } // uploadAsset uploads file to target and logs all actions -func uploadAsset(ctx *context.Context, instance config.Put, artifact artifact.Artifact, kind string, check ResponseChecker) error { - envName := fmt.Sprintf("%s_%s_SECRET", strings.ToUpper(kind), strings.ToUpper(instance.Name)) +func uploadAsset(ctx *context.Context, put config.Put, artifact artifact.Artifact, kind string, check ResponseChecker) error { + envName := fmt.Sprintf("%s_%s_SECRET", strings.ToUpper(kind), strings.ToUpper(put.Name)) secret := ctx.Env[envName] // Generate the target url - targetURL, err := resolveTargetTemplate(ctx, instance, artifact) + targetURL, err := resolveTargetTemplate(ctx, put, artifact) if err != nil { msg := fmt.Sprintf("%s: error while building the target url", kind) - log.WithField("instance", instance.Name).WithError(err).Error(msg) + log.WithField("instance", put.Name).WithError(err).Error(msg) return errors.Wrap(err, msg) } // Handle the artifact - file, err := os.Open(artifact.Path) + asset, err := assetOpen(kind, &artifact) if err != nil { return err } - defer file.Close() // nolint: errcheck + defer asset.ReadCloser.Close() // nolint: errcheck // The target url needs to contain the artifact name if !strings.HasSuffix(targetURL, "/") { @@ -159,19 +198,19 @@ func uploadAsset(ctx *context.Context, instance config.Put, artifact artifact.Ar } targetURL += artifact.Name - location, _, err := uploadAssetToServer(ctx, targetURL, instance.Username, secret, file, check) + location, _, err := uploadAssetToServer(ctx, targetURL, put.Username, secret, asset, check) if err != nil { msg := fmt.Sprintf("%s: upload failed", kind) log.WithError(err).WithFields(log.Fields{ - "instance": instance.Name, - "username": instance.Username, + "instance": put.Name, + "username": put.Username, }).Error(msg) return errors.Wrap(err, msg) } log.WithFields(log.Fields{ - "instance": instance.Name, - "mode": instance.Mode, + "instance": put.Name, + "mode": put.Mode, "uri": location, }).Info("uploaded successful") @@ -179,16 +218,8 @@ func uploadAsset(ctx *context.Context, instance config.Put, artifact artifact.Ar } // uploadAssetToServer uploads the asset file to target -func uploadAssetToServer(ctx *context.Context, target, username, secret string, file *os.File, check ResponseChecker) (string, *h.Response, error) { - stat, err := file.Stat() - if err != nil { - return "", nil, err - } - if stat.IsDir() { - return "", nil, errors.New("the asset to upload can't be a directory") - } - - req, err := newUploadRequest(target, username, secret, file, stat.Size()) +func uploadAssetToServer(ctx *context.Context, target, username, secret string, a *asset, check ResponseChecker) (string, *h.Response, error) { + req, err := newUploadRequest(target, username, secret, a) if err != nil { return "", nil, err } @@ -201,17 +232,17 @@ func uploadAssetToServer(ctx *context.Context, target, username, secret string, } // newUploadRequest creates a new h.Request for uploading -func newUploadRequest(target, username, secret string, reader io.Reader, size int64) (*h.Request, error) { +func newUploadRequest(target, username, secret string, a *asset) (*h.Request, error) { u, err := url.Parse(target) if err != nil { return nil, err } - req, err := h.NewRequest("PUT", u.String(), reader) + req, err := h.NewRequest("PUT", u.String(), a.ReadCloser) if err != nil { return nil, err } - req.ContentLength = size + req.ContentLength = a.Size req.SetBasicAuth(username, secret) return req, err diff --git a/internal/http/http_test.go b/internal/http/http_test.go new file mode 100644 index 000000000..871e46019 --- /dev/null +++ b/internal/http/http_test.go @@ -0,0 +1,252 @@ +package http + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + h "net/http" + "net/http/httptest" + "sync" + "testing" + + "github.com/pkg/errors" + + "github.com/goreleaser/goreleaser/config" + "github.com/goreleaser/goreleaser/context" + "github.com/goreleaser/goreleaser/internal/artifact" +) + +var ( + mux *h.ServeMux + srv *httptest.Server +) + +func setup() { + mux = h.NewServeMux() + srv = httptest.NewServer(mux) +} + +func teardown() { + srv.Close() +} + +func TestDefaults(t *testing.T) { + type args struct { + puts []config.Put + } + tests := []struct { + name string + args args + wantErr bool + wantMode string + }{ + {"set default", args{[]config.Put{{Name: "a", Target: "http://"}}}, false, ModeArchive}, + {"keep value", args{[]config.Put{{Name: "a", Target: "http://...", Mode: ModeBinary}}}, false, ModeBinary}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := Defaults(tt.args.puts); (err != nil) != tt.wantErr { + t.Errorf("Defaults() error = %v, wantErr %v", err, tt.wantErr) + } + if tt.wantMode != tt.args.puts[0].Mode { + t.Errorf("Incorrect Defaults() mode %q , wanted %q", tt.args.puts[0].Mode, tt.wantMode) + } + }) + } +} + +func TestCheckConfig(t *testing.T) { + ctx := context.New(config.Project{ProjectName: "blah"}) + ctx.Env["TEST_A_SECRET"] = "x" + type args struct { + ctx *context.Context + upload *config.Put + kind string + } + tests := []struct { + name string + args args + wantErr bool + }{ + {"ok", args{ctx, &config.Put{Name: "a", Target: "http://blabla", Username: "pepe", Mode: ModeArchive}, "test"}, false}, + {"secret missing", args{ctx, &config.Put{Name: "b", Target: "http://blabla", Username: "pepe", Mode: ModeArchive}, "test"}, true}, + {"target missing", args{ctx, &config.Put{Name: "a", Username: "pepe", Mode: ModeArchive}, "test"}, true}, + {"username missing", args{ctx, &config.Put{Name: "a", Target: "http://blabla", Mode: ModeArchive}, "test"}, true}, + {"name missing", args{ctx, &config.Put{Target: "http://blabla", Username: "pepe", Mode: ModeArchive}, "test"}, true}, + {"mode missing", args{ctx, &config.Put{Name: "a", Target: "http://blabla", Username: "pepe"}, "test"}, true}, + {"mode invalid", args{ctx, &config.Put{Name: "a", Target: "http://blabla", Username: "pepe", Mode: "blabla"}, "test"}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := CheckConfig(tt.args.ctx, tt.args.upload, tt.args.kind); (err != nil) != tt.wantErr { + t.Errorf("CheckConfig() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func count(r io.Reader) (int64, error) { + var ( + c int64 + b int64 + err error + buf = make([]byte, 16) + ) + for b >= 0 && err == nil { + b, err := r.Read(buf) + if err != nil { + return c, err + } + c = c + int64(b) + } + return c, nil +} + +type check struct { + path string + user string + pass string + content []byte +} + +func checks(checks ...check) func(rs []*h.Request) error { + return func(rs []*h.Request) error { + if len(rs) != len(checks) { + return errors.New("expectations mismatch requests") + } + for _, r := range rs { + found := false + for _, c := range checks { + if c.path == r.RequestURI { + found = true + err := doCheck(c, r) + if err != nil { + return err + } + break + } + } + if !found { + return errors.Errorf("check not found for request %+v", r) + } + } + return nil + } +} + +func doCheck(c check, r *h.Request) error { + contentLength := int64(len(c.content)) + if r.ContentLength != contentLength { + return errors.Errorf("request content-length header value %v unexpected, wanted %v", r.ContentLength, contentLength) + } + bs, err := ioutil.ReadAll(r.Body) + if err != nil { + return errors.Errorf("reading request body: %v", err) + } + if !bytes.Equal(bs, c.content) { + return errors.New("content does not match") + } + if int64(len(bs)) != contentLength { + return errors.Errorf("request content length %v unexpected, wanted %v", int64(len(bs)), contentLength) + } + if r.RequestURI != c.path { + return errors.Errorf("bad request uri %q, expecting %q", r.RequestURI, c.path) + } + if u, p, ok := r.BasicAuth(); !ok || u != c.user || p != c.pass { + return errors.Errorf("bad basic auth credentials: %s/%s", u, p) + } + return nil +} + +func TestUpload(t *testing.T) { + setup() + defer teardown() + content := []byte("blah!") + requests := []*h.Request{} + var m sync.Mutex + mux.Handle("/", h.HandlerFunc(func(w h.ResponseWriter, r *h.Request) { + bs, err := ioutil.ReadAll(r.Body) + if err != nil { + w.WriteHeader(h.StatusInternalServerError) + fmt.Fprintf(w, "reading request body: %v", err) + return + } + r.Body = ioutil.NopCloser(bytes.NewReader(bs)) + m.Lock() + requests = append(requests, r) + m.Unlock() + w.WriteHeader(h.StatusCreated) + w.Header().Set("Location", r.URL.RequestURI()) + })) + assetOpen = func(k string, a *artifact.Artifact) (*asset, error) { + return &asset{ + ReadCloser: ioutil.NopCloser(bytes.NewReader(content)), + Size: int64(len(content)), + }, nil + } + defer assetOpenReset() + var is2xx ResponseChecker = func(r *h.Response) (string, error) { + if r.StatusCode/100 == 2 { + return r.Header.Get("Location"), nil + } + return "", errors.Errorf("unexpected http status code: %v", r.StatusCode) + } + ctx := context.New(config.Project{ProjectName: "blah"}) + ctx.Env["TEST_A_SECRET"] = "x" + ctx.Version = "2.1.0" + ctx.Artifacts = artifact.New() + for _, a := range []struct { + ext string + typ artifact.Type + }{ + {"---", artifact.DockerImage}, + {"deb", artifact.LinuxPackage}, + {"bin", artifact.Binary}, + {"tar", artifact.UploadableArchive}, + {"ubi", artifact.UploadableBinary}, + {"sum", artifact.Checksum}, + {"sig", artifact.Signature}, + } { + ctx.Artifacts.Add(artifact.Artifact{Name: "a." + a.ext, Path: "/a/a." + a.ext, Type: a.typ}) + } + tests := []struct { + name string + ctx *context.Context + wantErr bool + put config.Put + check func(r []*h.Request) error + }{ + {"archive", ctx, false, + config.Put{Mode: ModeArchive, Name: "a", Target: srv.URL + "/{{.ProjectName}}/{{.Version}}/", Username: "u1"}, + checks( + check{"/blah/2.1.0/a.deb", "u1", "x", content}, + check{"/blah/2.1.0/a.tar", "u1", "x", content}, + ), + }, + {"binary", ctx, false, + config.Put{Mode: ModeBinary, Name: "a", Target: srv.URL + "/{{.ProjectName}}/{{.Version}}/", Username: "u2"}, + checks(check{"/blah/2.1.0/a.ubi", "u2", "x", content}), + }, + {"archive-with-checksum-and-signature", ctx, false, + config.Put{Mode: ModeArchive, Name: "a", Target: srv.URL + "/{{.ProjectName}}/{{.Version}}/", Username: "u3", Checksum: true, Signature: true}, + checks( + check{"/blah/2.1.0/a.deb", "u3", "x", content}, + check{"/blah/2.1.0/a.tar", "u3", "x", content}, + check{"/blah/2.1.0/a.sum", "u3", "x", content}, + check{"/blah/2.1.0/a.sig", "u3", "x", content}, + ), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + requests = nil + if err := Upload(tt.ctx, []config.Put{tt.put}, "test", is2xx); (err != nil) != tt.wantErr { + t.Errorf("Upload() error = %v, wantErr %v", err, tt.wantErr) + } + if err := tt.check(requests); err != nil { + t.Errorf("Upload() request invalid. Error: %v", err) + } + }) + } +}