go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cipd/appengine/impl/gs/gs_test.go (about)

     1  // Copyright 2017 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package gs
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"io"
    21  	"net/http"
    22  	"net/http/httptest"
    23  	"net/url"
    24  	"sync"
    25  	"testing"
    26  	"time"
    27  
    28  	"go.chromium.org/luci/common/clock"
    29  	"go.chromium.org/luci/common/clock/testclock"
    30  
    31  	. "github.com/smartystreets/goconvey/convey"
    32  	. "go.chromium.org/luci/common/testing/assertions"
    33  )
    34  
    35  func TestImpl(t *testing.T) {
    36  	t.Parallel()
    37  
    38  	Convey("With mocked service", t, func(c C) {
    39  		ctx, cl := testclock.UseTime(context.Background(), testclock.TestRecentTimeUTC)
    40  		cl.SetTimerCallback(func(d time.Duration, t clock.Timer) { cl.Add(d) })
    41  
    42  		type call struct {
    43  			Method   string
    44  			Path     string
    45  			Query    url.Values
    46  			Range    string // value of Range request header
    47  			Code     int
    48  			Response any
    49  			Location string // value of Location response header
    50  			NonJSON  bool   // if true, do not put alt=json in the expected URL
    51  		}
    52  
    53  		lock := sync.Mutex{}
    54  		expected := []call{}
    55  
    56  		expect := func(c call) {
    57  			lock.Lock()
    58  			defer lock.Unlock()
    59  			if c.Query == nil {
    60  				c.Query = url.Values{}
    61  			}
    62  			if c.Query.Get("alt") == "" && !c.NonJSON {
    63  				c.Query.Set("alt", "json")
    64  			}
    65  			if c.Response == nil {
    66  				c.Response = map[string]string{"size": "123"}
    67  			}
    68  			expected = append(expected, c)
    69  		}
    70  
    71  		srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    72  			lock.Lock()
    73  			next := call{Method: "???", Path: "???"}
    74  			if len(expected) > 0 {
    75  				next = expected[0]
    76  				expected = expected[1:]
    77  			}
    78  			lock.Unlock()
    79  
    80  			q := r.URL.Query()
    81  			q.Del("prettyPrint") // not really relevant to anything
    82  
    83  			c.So(r.Method+" "+r.URL.Path, ShouldEqual, next.Method+" "+next.Path)
    84  			c.So(q, ShouldResemble, next.Query)
    85  			c.So(r.Header.Get("Range"), ShouldEqual, next.Range)
    86  
    87  			var response []byte
    88  			var err error
    89  
    90  			if next.Response != nil {
    91  				if str, yep := next.Response.(string); yep {
    92  					w.Header().Set("Content-Type", "application/octet-stream")
    93  					response = []byte(str)
    94  				} else {
    95  					w.Header().Set("Content-Type", "application/json")
    96  					response, err = json.Marshal(next.Response)
    97  					c.So(err, ShouldBeNil)
    98  				}
    99  			}
   100  
   101  			if next.Location != "" {
   102  				w.Header().Set("Location", next.Location)
   103  			}
   104  			if next.Code != 0 {
   105  				w.WriteHeader(next.Code)
   106  			}
   107  			if next.Response != nil {
   108  				w.Write(response)
   109  			}
   110  		}))
   111  		defer srv.Close()
   112  
   113  		gs := &impl{
   114  			ctx:              ctx,
   115  			testingTransport: http.DefaultTransport,
   116  			testingBasePath:  srv.URL,
   117  		}
   118  
   119  		Convey("Size - exists", func() {
   120  			expect(call{
   121  				Method: "GET",
   122  				Path:   "/b/bucket/o/a/b/c",
   123  			})
   124  			s, yes, err := gs.Size(ctx, "/bucket/a/b/c")
   125  			So(err, ShouldBeNil)
   126  			So(s, ShouldEqual, 123)
   127  			So(yes, ShouldBeTrue)
   128  		})
   129  
   130  		Convey("Size - missing", func() {
   131  			expect(call{
   132  				Method: "GET",
   133  				Path:   "/b/bucket/o/a/b/c",
   134  				Code:   http.StatusNotFound,
   135  			})
   136  			s, yes, err := gs.Size(ctx, "/bucket/a/b/c")
   137  			So(err, ShouldBeNil)
   138  			So(s, ShouldEqual, 0)
   139  			So(yes, ShouldBeFalse)
   140  		})
   141  
   142  		Convey("Size - error", func() {
   143  			expect(call{
   144  				Method: "GET",
   145  				Path:   "/b/bucket/o/a/b/c",
   146  				Code:   http.StatusForbidden,
   147  			})
   148  			s, yes, err := gs.Size(ctx, "/bucket/a/b/c")
   149  			So(StatusCode(err), ShouldEqual, http.StatusForbidden)
   150  			So(s, ShouldEqual, 0)
   151  			So(yes, ShouldBeFalse)
   152  		})
   153  
   154  		Convey("Copy unconditional", func() {
   155  			expect(call{
   156  				Method: "POST",
   157  				Path:   "/b/src_bucket/o/src_obj/copyTo/b/dst_bucket/o/dst_obj",
   158  			})
   159  			So(gs.Copy(ctx, "/dst_bucket/dst_obj", -1, "/src_bucket/src_obj", -1), ShouldBeNil)
   160  		})
   161  
   162  		Convey("Copy conditional", func() {
   163  			expect(call{
   164  				Method: "POST",
   165  				Path:   "/b/src_bucket/o/src_obj/copyTo/b/dst_bucket/o/dst_obj",
   166  				Query: url.Values{
   167  					"ifSourceGenerationMatch": {"1"},
   168  					"ifGenerationMatch":       {"2"},
   169  				},
   170  			})
   171  			So(gs.Copy(ctx, "/dst_bucket/dst_obj", 2, "/src_bucket/src_obj", 1), ShouldBeNil)
   172  		})
   173  
   174  		Convey("Copy error", func() {
   175  			expect(call{
   176  				Method: "POST",
   177  				Path:   "/b/src_bucket/o/src_obj/copyTo/b/dst_bucket/o/dst_obj",
   178  				Code:   http.StatusForbidden,
   179  			})
   180  			err := gs.Copy(ctx, "/dst_bucket/dst_obj", -1, "/src_bucket/src_obj", -1)
   181  			So(StatusCode(err), ShouldEqual, http.StatusForbidden)
   182  		})
   183  
   184  		Convey("Delete present", func() {
   185  			expect(call{
   186  				Method: "DELETE",
   187  				Path:   "/b/bucket/o/a/b/c",
   188  			})
   189  			So(gs.Delete(ctx, "/bucket/a/b/c"), ShouldBeNil)
   190  		})
   191  
   192  		Convey("Delete missing", func() {
   193  			expect(call{
   194  				Method: "DELETE",
   195  				Path:   "/b/bucket/o/a/b/c",
   196  				Code:   http.StatusNotFound,
   197  			})
   198  			So(gs.Delete(ctx, "/bucket/a/b/c"), ShouldBeNil)
   199  		})
   200  
   201  		Convey("Delete error", func() {
   202  			expect(call{
   203  				Method: "DELETE",
   204  				Path:   "/b/bucket/o/a/b/c",
   205  				Code:   http.StatusForbidden,
   206  			})
   207  			So(StatusCode(gs.Delete(ctx, "/bucket/a/b/c")), ShouldEqual, http.StatusForbidden)
   208  		})
   209  
   210  		Convey("Publish success", func() {
   211  			expect(call{
   212  				Method: "POST",
   213  				Path:   "/b/src_bucket/o/src_obj/copyTo/b/dst_bucket/o/dst_obj",
   214  				Query: url.Values{
   215  					"ifGenerationMatch":       {"0"},
   216  					"ifSourceGenerationMatch": {"1"},
   217  				},
   218  			})
   219  			So(gs.Publish(ctx, "/dst_bucket/dst_obj", "/src_bucket/src_obj", 1), ShouldBeNil)
   220  		})
   221  
   222  		Convey("Publish bad precondition on srcGen", func() {
   223  			expect(call{
   224  				Method: "POST",
   225  				Path:   "/b/src_bucket/o/src_obj/copyTo/b/dst_bucket/o/dst_obj",
   226  				Query: url.Values{
   227  					"ifGenerationMatch":       {"0"},
   228  					"ifSourceGenerationMatch": {"1"},
   229  				},
   230  				Code: http.StatusPreconditionFailed,
   231  			})
   232  			expect(call{
   233  				Method: "GET",
   234  				Path:   "/b/dst_bucket/o/dst_obj",
   235  				Code:   http.StatusNotFound,
   236  			})
   237  			err := gs.Publish(ctx, "/dst_bucket/dst_obj", "/src_bucket/src_obj", 1)
   238  			So(StatusCode(err), ShouldEqual, http.StatusPreconditionFailed)
   239  			So(err, ShouldErrLike, "unexpected generation number")
   240  		})
   241  
   242  		Convey("Publish general error", func() {
   243  			expect(call{
   244  				Method: "POST",
   245  				Path:   "/b/src_bucket/o/src_obj/copyTo/b/dst_bucket/o/dst_obj",
   246  				Query: url.Values{
   247  					"ifGenerationMatch":       {"0"},
   248  					"ifSourceGenerationMatch": {"1"},
   249  				},
   250  				Code: http.StatusForbidden,
   251  			})
   252  			err := gs.Publish(ctx, "/dst_bucket/dst_obj", "/src_bucket/src_obj", 1)
   253  			So(StatusCode(err), ShouldEqual, http.StatusForbidden)
   254  		})
   255  
   256  		Convey("Publish missing source object", func() {
   257  			expect(call{
   258  				Method: "POST",
   259  				Path:   "/b/src_bucket/o/src_obj/copyTo/b/dst_bucket/o/dst_obj",
   260  				Query: url.Values{
   261  					"ifGenerationMatch":       {"0"},
   262  					"ifSourceGenerationMatch": {"1"},
   263  				},
   264  				Code: http.StatusNotFound,
   265  			})
   266  			expect(call{
   267  				Method: "GET",
   268  				Path:   "/b/dst_bucket/o/dst_obj",
   269  				Code:   http.StatusNotFound,
   270  			})
   271  			err := gs.Publish(ctx, "/dst_bucket/dst_obj", "/src_bucket/src_obj", 1)
   272  			So(StatusCode(err), ShouldEqual, http.StatusNotFound)
   273  			So(err, ShouldErrLike, "the source object is missing")
   274  		})
   275  
   276  		Convey("Publish already published (failed precondition on dstGen)", func() {
   277  			expect(call{
   278  				Method: "POST",
   279  				Path:   "/b/src_bucket/o/src_obj/copyTo/b/dst_bucket/o/dst_obj",
   280  				Query: url.Values{
   281  					"ifGenerationMatch":       {"0"},
   282  					"ifSourceGenerationMatch": {"1"},
   283  				},
   284  				Code: http.StatusPreconditionFailed,
   285  			})
   286  			expect(call{
   287  				Method: "GET",
   288  				Path:   "/b/dst_bucket/o/dst_obj",
   289  			})
   290  			So(gs.Publish(ctx, "/dst_bucket/dst_obj", "/src_bucket/src_obj", 1), ShouldBeNil)
   291  		})
   292  
   293  		Convey("Publish already published, only srcDst precondition", func() {
   294  			expect(call{
   295  				Method: "POST",
   296  				Path:   "/b/src_bucket/o/src_obj/copyTo/b/dst_bucket/o/dst_obj",
   297  				Query: url.Values{
   298  					"ifGenerationMatch": {"0"},
   299  				},
   300  				Code: http.StatusPreconditionFailed,
   301  			})
   302  			So(gs.Publish(ctx, "/dst_bucket/dst_obj", "/src_bucket/src_obj", -1), ShouldBeNil)
   303  		})
   304  
   305  		Convey("Publish error when checking presence", func() {
   306  			expect(call{
   307  				Method: "POST",
   308  				Path:   "/b/src_bucket/o/src_obj/copyTo/b/dst_bucket/o/dst_obj",
   309  				Query: url.Values{
   310  					"ifGenerationMatch":       {"0"},
   311  					"ifSourceGenerationMatch": {"1"},
   312  				},
   313  				Code: http.StatusNotFound,
   314  			})
   315  			expect(call{
   316  				Method: "GET",
   317  				Path:   "/b/dst_bucket/o/dst_obj",
   318  				Code:   http.StatusForbidden,
   319  			})
   320  			err := gs.Publish(ctx, "/dst_bucket/dst_obj", "/src_bucket/src_obj", 1)
   321  			So(StatusCode(err), ShouldEqual, http.StatusForbidden)
   322  		})
   323  
   324  		Convey("StartUpload success", func() {
   325  			expect(call{
   326  				Method: "POST",
   327  				Path:   "/upload/storage/v1/b/bucket/o",
   328  				Query: url.Values{
   329  					"name":       {"a/b/c"},
   330  					"uploadType": {"resumable"},
   331  				},
   332  				Location: "http://upload-session.example.com/a/b/c",
   333  			})
   334  			url, err := gs.StartUpload(ctx, "/bucket/a/b/c")
   335  			So(err, ShouldBeNil)
   336  			So(url, ShouldEqual, "http://upload-session.example.com/a/b/c")
   337  		})
   338  
   339  		Convey("StartUpload error", func() {
   340  			expect(call{
   341  				Method: "POST",
   342  				Path:   "/upload/storage/v1/b/bucket/o",
   343  				Query: url.Values{
   344  					"name":       {"a/b/c"},
   345  					"uploadType": {"resumable"},
   346  				},
   347  				Code: http.StatusForbidden,
   348  			})
   349  			url, err := gs.StartUpload(ctx, "/bucket/a/b/c")
   350  			So(StatusCode(err), ShouldEqual, http.StatusForbidden)
   351  			So(url, ShouldEqual, "")
   352  		})
   353  
   354  		Convey("CancelUpload success", func() {
   355  			expect(call{
   356  				Method:  "DELETE",
   357  				Path:    "/upload_url",
   358  				NonJSON: true,
   359  				Code:    499,
   360  			})
   361  			So(gs.CancelUpload(ctx, srv.URL+"/upload_url"), ShouldBeNil)
   362  		})
   363  
   364  		Convey("CancelUpload error", func() {
   365  			expect(call{
   366  				Method:  "DELETE",
   367  				Path:    "/upload_url",
   368  				NonJSON: true,
   369  				Code:    400,
   370  			})
   371  			So(gs.CancelUpload(ctx, srv.URL+"/upload_url"), ShouldNotBeNil)
   372  		})
   373  
   374  		Convey("Reader works", func() {
   375  			expect(call{
   376  				Method: "GET",
   377  				Path:   "/b/bucket/o/a/b/c",
   378  				Response: map[string]string{
   379  					"generation": "123",
   380  					"size":       "1000",
   381  				},
   382  			})
   383  			r, err := gs.Reader(ctx, "/bucket/a/b/c", 0)
   384  			So(err, ShouldBeNil)
   385  			So(r, ShouldNotBeNil)
   386  
   387  			So(r.Generation(), ShouldEqual, 123)
   388  			So(r.Size(), ShouldEqual, 1000)
   389  
   390  			// Read from the middle.
   391  			expect(call{
   392  				Method: "GET",
   393  				Path:   "/b/bucket/o/a/b/c",
   394  				Query: url.Values{
   395  					"alt":        {"media"},
   396  					"generation": {"123"},
   397  				},
   398  				Range:    "bytes=100-104",
   399  				Response: "12345",
   400  			})
   401  			buf := make([]byte, 5)
   402  			n, err := r.ReadAt(buf, 100)
   403  			So(err, ShouldBeNil)
   404  			So(n, ShouldEqual, 5)
   405  			So(string(buf), ShouldEqual, "12345")
   406  
   407  			// Read close to the end.
   408  			expect(call{
   409  				Method: "GET",
   410  				Path:   "/b/bucket/o/a/b/c",
   411  				Query: url.Values{
   412  					"alt":        {"media"},
   413  					"generation": {"123"},
   414  				},
   415  				Range:    "bytes=998-999",
   416  				Response: "12",
   417  			})
   418  			buf = make([]byte, 5)
   419  			n, err = r.ReadAt(buf, 998)
   420  			So(err, ShouldEqual, io.EOF)
   421  			So(n, ShouldEqual, 2)
   422  			So(string(buf), ShouldEqual, "12\x00\x00\x00")
   423  
   424  			// Read past the end.
   425  			n, err = r.ReadAt(buf, 1000)
   426  			So(err, ShouldEqual, io.EOF)
   427  			So(n, ShouldEqual, 0)
   428  		})
   429  
   430  		Convey("Reader with generation", func() {
   431  			expect(call{
   432  				Method: "GET",
   433  				Path:   "/b/bucket/o/a/b/c",
   434  				Query: url.Values{
   435  					"generation": {"123"},
   436  				},
   437  				Response: map[string]string{
   438  					"generation": "123",
   439  					"size":       "1000",
   440  				},
   441  			})
   442  			r, err := gs.Reader(ctx, "/bucket/a/b/c", 123)
   443  			So(err, ShouldBeNil)
   444  			So(r, ShouldNotBeNil)
   445  			So(r.Generation(), ShouldEqual, 123)
   446  			So(r.Size(), ShouldEqual, 1000)
   447  		})
   448  	})
   449  }