package api_test import ( "encoding/base64" "encoding/json" "fmt" "io/ioutil" "net/http" "net/http/httptest" "net/url" "os" "strconv" "strings" "testing" "github.com/github/git-lfs/api" "github.com/github/git-lfs/auth" "github.com/github/git-lfs/config" "github.com/github/git-lfs/errutil" "github.com/github/git-lfs/httputil" ) func TestSuccessfulDownload(t *testing.T) { SetupTestCredentialsFunc() defer func() { RestoreCredentialsFunc() }() mux := http.NewServeMux() server := httptest.NewServer(mux) defer server.Close() tmp := tempdir(t) defer os.RemoveAll(tmp) mux.HandleFunc("/media/objects/oid", func(w http.ResponseWriter, r *http.Request) { t.Logf("Server: %s %s", r.Method, r.URL) t.Logf("request header: %v", r.Header) if r.Method != "GET" { w.WriteHeader(405) return } if r.Header.Get("Accept") != api.MediaType { t.Error("Invalid Accept") } if r.Header.Get("Authorization") != expectedAuth(t, server) { t.Error("Invalid Authorization") } obj := &api.ObjectResource{ Oid: "oid", Size: 4, Actions: map[string]*api.LinkRelation{ "download": &api.LinkRelation{ Href: server.URL + "/download", Header: map[string]string{"A": "1"}, }, }, } by, err := json.Marshal(obj) if err != nil { t.Fatal(err) } head := w.Header() head.Set("Content-Type", api.MediaType) head.Set("Content-Length", strconv.Itoa(len(by))) w.WriteHeader(200) w.Write(by) }) defer config.Config.ResetConfig() config.Config.SetConfig("lfs.batch", "false") config.Config.SetConfig("lfs.url", server.URL+"/media") obj, err := api.BatchOrLegacySingle(&api.ObjectResource{Oid: "oid"}, "download", []string{"basic"}) if err != nil { if isDockerConnectionError(err) { return } t.Fatalf("unexpected error: %s", err) } if obj.Size != 4 { t.Errorf("unexpected size: %d", obj.Size) } } // nearly identical to TestSuccessfulDownload // called multiple times to return different 3xx status codes func TestSuccessfulDownloadWithRedirects(t *testing.T) { SetupTestCredentialsFunc() defer func() { RestoreCredentialsFunc() }() mux := http.NewServeMux() server := httptest.NewServer(mux) defer server.Close() tmp := tempdir(t) defer os.RemoveAll(tmp) // all of these should work for GET requests redirectCodes := []int{301, 302, 303, 307} redirectIndex := 0 mux.HandleFunc("/redirect/objects/oid", func(w http.ResponseWriter, r *http.Request) { t.Logf("Server: %s %s", r.Method, r.URL) t.Logf("request header: %v", r.Header) if r.Method != "GET" { w.WriteHeader(405) return } w.Header().Set("Location", server.URL+"/redirect2/objects/oid") w.WriteHeader(redirectCodes[redirectIndex]) t.Logf("redirect with %d", redirectCodes[redirectIndex]) }) mux.HandleFunc("/redirect2/objects/oid", func(w http.ResponseWriter, r *http.Request) { t.Logf("Server: %s %s", r.Method, r.URL) t.Logf("request header: %v", r.Header) if r.Method != "GET" { w.WriteHeader(405) return } w.Header().Set("Location", server.URL+"/media/objects/oid") w.WriteHeader(redirectCodes[redirectIndex]) t.Logf("redirect again with %d", redirectCodes[redirectIndex]) redirectIndex += 1 }) mux.HandleFunc("/media/objects/oid", func(w http.ResponseWriter, r *http.Request) { t.Logf("Server: %s %s", r.Method, r.URL) t.Logf("request header: %v", r.Header) if r.Method != "GET" { w.WriteHeader(405) return } if r.Header.Get("Accept") != api.MediaType { t.Error("Invalid Accept") } if r.Header.Get("Authorization") != expectedAuth(t, server) { t.Error("Invalid Authorization") } obj := &api.ObjectResource{ Oid: "oid", Size: 4, Actions: map[string]*api.LinkRelation{ "download": &api.LinkRelation{ Href: server.URL + "/download", Header: map[string]string{"A": "1"}, }, }, } by, err := json.Marshal(obj) if err != nil { t.Fatal(err) } head := w.Header() head.Set("Content-Type", api.MediaType) head.Set("Content-Length", strconv.Itoa(len(by))) w.WriteHeader(200) w.Write(by) }) defer config.Config.ResetConfig() config.Config.SetConfig("lfs.batch", "false") config.Config.SetConfig("lfs.url", server.URL+"/redirect") for _, redirect := range redirectCodes { obj, err := api.BatchOrLegacySingle(&api.ObjectResource{Oid: "oid"}, "download", []string{"basic"}) if err != nil { if isDockerConnectionError(err) { return } t.Fatalf("unexpected error for %d status: %s", redirect, err) } if obj.Size != 4 { t.Errorf("unexpected size for %d status: %d", redirect, obj.Size) } } } // nearly identical to TestSuccessfulDownload // the api request returns a custom Authorization header func TestSuccessfulDownloadWithAuthorization(t *testing.T) { SetupTestCredentialsFunc() defer func() { RestoreCredentialsFunc() }() mux := http.NewServeMux() server := httptest.NewServer(mux) defer server.Close() tmp := tempdir(t) defer os.RemoveAll(tmp) mux.HandleFunc("/media/objects/oid", func(w http.ResponseWriter, r *http.Request) { t.Logf("Server: %s %s", r.Method, r.URL) t.Logf("request header: %v", r.Header) if r.Method != "GET" { w.WriteHeader(405) return } if r.Header.Get("Accept") != api.MediaType { t.Error("Invalid Accept") } if r.Header.Get("Authorization") != expectedAuth(t, server) { t.Error("Invalid Authorization") } obj := &api.ObjectResource{ Oid: "oid", Size: 4, Actions: map[string]*api.LinkRelation{ "download": &api.LinkRelation{ Href: server.URL + "/download", Header: map[string]string{ "A": "1", "Authorization": "custom", }, }, }, } by, err := json.Marshal(obj) if err != nil { t.Fatal(err) } head := w.Header() head.Set("Content-Type", "application/json; charset=utf-8") head.Set("Content-Length", strconv.Itoa(len(by))) w.WriteHeader(200) w.Write(by) }) defer config.Config.ResetConfig() config.Config.SetConfig("lfs.batch", "false") config.Config.SetConfig("lfs.url", server.URL+"/media") obj, err := api.BatchOrLegacySingle(&api.ObjectResource{Oid: "oid"}, "download", []string{"basic"}) if err != nil { if isDockerConnectionError(err) { return } t.Fatalf("unexpected error: %s", err) } if obj.Size != 4 { t.Errorf("unexpected size: %d", obj.Size) } } func TestDownloadAPIError(t *testing.T) { SetupTestCredentialsFunc() defer func() { RestoreCredentialsFunc() }() mux := http.NewServeMux() server := httptest.NewServer(mux) defer server.Close() tmp := tempdir(t) defer os.RemoveAll(tmp) mux.HandleFunc("/media/objects/oid", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(404) }) defer config.Config.ResetConfig() config.Config.SetConfig("lfs.batch", "false") config.Config.SetConfig("lfs.url", server.URL+"/media") _, err := api.BatchOrLegacySingle(&api.ObjectResource{Oid: "oid"}, "download", []string{"basic"}) if err == nil { t.Fatal("no error?") } if errutil.IsFatalError(err) { t.Fatal("should not panic") } if isDockerConnectionError(err) { return } if err.Error() != fmt.Sprintf(httputil.GetDefaultError(404), server.URL+"/media/objects/oid") { t.Fatalf("Unexpected error: %s", err.Error()) } } // guards against connection errors that only seem to happen on debian docker // images. func isDockerConnectionError(err error) bool { if err == nil { return false } if os.Getenv("TRAVIS") == "true" { return false } e := err.Error() return strings.Contains(e, "connection reset by peer") || strings.Contains(e, "connection refused") } func tempdir(t *testing.T) string { dir, err := ioutil.TempDir("", "git-lfs-test") if err != nil { t.Fatalf("Error getting temp dir: %s", err) } return dir } func expectedAuth(t *testing.T, server *httptest.Server) string { u, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } token := fmt.Sprintf("%s:%s", u.Host, "monkey") return "Basic " + strings.TrimSpace(base64.StdEncoding.EncodeToString([]byte(token))) } var ( TestCredentialsFunc auth.CredentialFunc origCredentialsFunc auth.CredentialFunc ) func init() { TestCredentialsFunc = func(input auth.Creds, subCommand string) (auth.Creds, error) { output := make(auth.Creds) for key, value := range input { output[key] = value } if _, ok := output["username"]; !ok { output["username"] = input["host"] } output["password"] = "monkey" return output, nil } } // Override the credentials func for testing func SetupTestCredentialsFunc() { origCredentialsFunc = auth.SetCredentialsFunc(TestCredentialsFunc) } // Put the original credentials func back func RestoreCredentialsFunc() { auth.SetCredentialsFunc(origCredentialsFunc) }