Repository: fsouza/fake-gcs-server
Pull request #40 (2019-06-12 16:25:03)

Add optional custom public host
Add the ability to specify a different public host than ``storage.googleapis.com``. This would be used by https://github.com/teone/gc-fake-storage to serve files.  This is set before ``buildMuxer()`` is called, and can be returned by ``PublicURL()``.


Patch:
diff --git a/fakestorage/server.go b/fakestorage/server.go
index e9b25f9..f6bb37c 100644
--- a/fakestorage/server.go
+++ b/fakestorage/server.go
@@ -29,6 +29,7 @@ type Server struct {
 	ts          *httptest.Server
 	mux         *mux.Router
 	externalURL string
+	publicHost  string
 }
 
 // NewServer creates a new instance of the server, pre-loaded with the given
@@ -60,14 +61,24 @@ type Options struct {
 	// client requests will get processed by an internal mocked transport.
 	NoListener bool
 
-	// optional external URL, such as http://gcs.example.com
-	// returned as Location header for resumable uploads
+	// Optional external URL, such as https://gcs.127.0.0.1.nip.io:4443
+	// Returned in the Location header for resumable uploads
+	// The "real" value is https://www.googleapis.com, the JSON API
+	// The default is whatever the server is bound to, such as https://0.0.0.0:4443
 	ExternalURL string
+
+	// Optional URL for public access
+	// An example is "storage.gcs.127.0.0.1.nip.io:4443", which will configure
+	// the server to serve objects at:
+	// https://storage.gcs.127.0.0.1.nip.io:4443/<bucket>/<object>
+	// https://<bucket>.storage.gcs.127.0.0.1.nip.io:4443>/<bucket>/<object>
+	// If unset, the default is "storage.googleapis.com", the XML API
+	PublicHost string
 }
 
 // NewServerWithOptions creates a new server with custom options
 func NewServerWithOptions(options Options) (*Server, error) {
-	s, err := newServer(options.InitialObjects, options.StorageRoot)
+	s, err := newServer(options.InitialObjects, options.StorageRoot, options.ExternalURL, options.PublicHost)
 	if err != nil {
 		return nil, err
 	}
@@ -75,9 +86,6 @@ func NewServerWithOptions(options Options) (*Server, error) {
 		s.setTransportToMux()
 		return s, nil
 	}
-	if options.ExternalURL != "" {
-		s.externalURL = options.ExternalURL
-	}
 
 	s.ts = httptest.NewUnstartedServer(s.mux)
 	if options.Port != 0 {
@@ -96,7 +104,7 @@ func NewServerWithOptions(options Options) (*Server, error) {
 	return s, nil
 }
 
-func newServer(objects []Object, storageRoot string) (*Server, error) {
+func newServer(objects []Object, storageRoot, externalURL, publicHost string) (*Server, error) {
 	backendObjects := toBackendObjects(objects)
 	var backendStorage backend.Storage
 	var err error
@@ -108,9 +116,14 @@ func newServer(objects []Object, storageRoot string) (*Server, error) {
 	if err != nil {
 		return nil, err
 	}
+	if publicHost == "" {
+		publicHost = "storage.googleapis.com"
+	}
 	s := Server{
-		backend: backendStorage,
-		uploads: sync.Map{},
+		backend:     backendStorage,
+		uploads:     sync.Map{},
+		externalURL: externalURL,
+		publicHost:  publicHost,
 	}
 	s.buildMuxer()
 	return &s, nil
@@ -133,8 +146,9 @@ func (s *Server) setTransportToMux() {
 
 func (s *Server) buildMuxer() {
 	s.mux = mux.NewRouter()
-	s.mux.Host("storage.googleapis.com").Path("/{bucketName}/{objectName:.+}").Methods("GET", "HEAD").HandlerFunc(s.downloadObject)
-	s.mux.Host("{bucketName}.storage.googleapis.com").Path("/{objectName:.+}").Methods("GET", "HEAD").HandlerFunc(s.downloadObject)
+	s.mux.Host(s.publicHost).Path("/{bucketName}/{objectName:.+}").Methods("GET", "HEAD").HandlerFunc(s.downloadObject)
+	bucketHost := fmt.Sprintf("{bucketName}.%s", s.publicHost)
+	s.mux.Host(bucketHost).Path("/{objectName:.+}").Methods("GET", "HEAD").HandlerFunc(s.downloadObject)
 	r := s.mux.PathPrefix("/storage/v1").Subrouter()
 	r.Path("/b").Methods("GET").HandlerFunc(s.listBuckets)
 	r.Path("/b").Methods("POST").HandlerFunc(s.createBucketByPost)
@@ -170,6 +184,11 @@ func (s *Server) URL() string {
 	return ""
 }
 
+// PublicURL returns the server's public download URL.
+func (s *Server) PublicURL() string {
+	return fmt.Sprintf("https://%s", s.publicHost)
+}
+
 // HTTPClient returns an HTTP client configured to talk to the server.
 func (s *Server) HTTPClient() *http.Client {
 	return &http.Client{Transport: s.transport}


Test patch:
diff --git a/fakestorage/server_test.go b/fakestorage/server_test.go
index dea6485..c281ce3 100644
--- a/fakestorage/server_test.go
+++ b/fakestorage/server_test.go
@@ -98,6 +98,95 @@ func testDownloadObject(t *testing.T, server *Server) {
 			"",
 		},
 	}
+	expected := "https://storage.googleapis.com"
+	if server.PublicURL() != expected {
+		t.Fatalf("Expected PublicURL \"%s\", is \"%s\".", expected, server.PublicURL())
+	}
+	for _, test := range tests {
+		test := test
+		t.Run(test.name, func(t *testing.T) {
+			client := server.HTTPClient()
+			req, err := http.NewRequest(test.method, test.url, nil)
+			if err != nil {
+				t.Fatal(err)
+			}
+			resp, err := client.Do(req)
+			if err != nil {
+				t.Fatal(err)
+			}
+			defer resp.Body.Close()
+			if resp.StatusCode != http.StatusOK {
+				t.Errorf("wrong status returned\nwant %d\ngot  %d", http.StatusOK, resp.StatusCode)
+			}
+			for k, expectedV := range test.expectedHeaders {
+				if v := resp.Header.Get(k); v != expectedV {
+					t.Errorf("wrong value for header %q:\nwant %q\ngot  %q", k, expectedV, v)
+				}
+			}
+			data, err := ioutil.ReadAll(resp.Body)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if body := string(data); body != test.expectedBody {
+				t.Errorf("wrong body\nwant %q\ngot  %q", test.expectedBody, body)
+			}
+		})
+	}
+}
+
+func TestDownloadObjectAlternatePublicHost(t *testing.T) {
+	var tests = []struct {
+		name            string
+		method          string
+		url             string
+		expectedHeaders map[string]string
+		expectedBody    string
+	}{
+		{
+			"GET: bucket in the path",
+			http.MethodGet,
+			"https://storage.gcs.127.0.0.1.nip.io:4443/some-bucket/files/txt/text-01.txt",
+			map[string]string{"accept-ranges": "bytes", "content-length": "9"},
+			"something",
+		},
+		{
+			"GET: bucket in the host",
+			http.MethodGet,
+			"https://other-bucket.storage.gcs.127.0.0.1.nip.io:4443/static/css/website.css",
+			map[string]string{"accept-ranges": "bytes", "content-length": "21"},
+			"body {display: none;}",
+		},
+		{
+			"HEAD: bucket in the path",
+			http.MethodHead,
+			"https://storage.gcs.127.0.0.1.nip.io:4443/some-bucket/files/txt/text-01.txt",
+			map[string]string{"accept-ranges": "bytes", "content-length": "9"},
+			"",
+		},
+		{
+			"HEAD: bucket in the host",
+			http.MethodHead,
+			"https://other-bucket.storage.gcs.127.0.0.1.nip.io:4443/static/css/website.css",
+			map[string]string{"accept-ranges": "bytes", "content-length": "21"},
+			"",
+		},
+	}
+	objs := []Object{
+		{BucketName: "some-bucket", Name: "files/txt/text-01.txt", Content: []byte("something")},
+		{BucketName: "other-bucket", Name: "static/css/website.css", Content: []byte("body {display: none;}")},
+	}
+	opts := Options{
+		InitialObjects: objs,
+		PublicHost:     "storage.gcs.127.0.0.1.nip.io:4443",
+	}
+	server, err := NewServerWithOptions(opts)
+	if err != nil {
+		t.Fatal(err)
+	}
+	expected := "https://storage.gcs.127.0.0.1.nip.io:4443"
+	if server.PublicURL() != expected {
+		t.Fatalf("Expected PublicURL \"%s\", is \"%s\".", expected, server.PublicURL())
+	}
 	for _, test := range tests {
 		test := test
 		t.Run(test.name, func(t *testing.T) {