go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/artifactcontent/server_test.go (about)

     1  // Copyright 2020 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 artifactcontent
    16  
    17  import (
    18  	"context"
    19  	"io"
    20  	"net/http"
    21  	"net/http/httptest"
    22  	"testing"
    23  	"time"
    24  
    25  	"google.golang.org/genproto/googleapis/bytestream"
    26  	"google.golang.org/grpc/codes"
    27  	"google.golang.org/grpc/status"
    28  
    29  	"go.chromium.org/luci/auth/identity"
    30  	"go.chromium.org/luci/common/clock"
    31  	"go.chromium.org/luci/common/clock/testclock"
    32  	"go.chromium.org/luci/server/auth"
    33  	"go.chromium.org/luci/server/auth/authtest"
    34  	"go.chromium.org/luci/server/router"
    35  	"go.chromium.org/luci/server/secrets"
    36  	"go.chromium.org/luci/server/secrets/testsecrets"
    37  
    38  	artifactcontenttest "go.chromium.org/luci/resultdb/internal/artifactcontent/testutil"
    39  	"go.chromium.org/luci/resultdb/internal/testutil"
    40  	"go.chromium.org/luci/resultdb/internal/testutil/insert"
    41  	pb "go.chromium.org/luci/resultdb/proto/v1"
    42  
    43  	. "github.com/smartystreets/goconvey/convey"
    44  )
    45  
    46  func TestGenerateSignedURL(t *testing.T) {
    47  	Convey(`TestGenerateSignedURL`, t, func(c C) {
    48  		ctx := testutil.TestingContext()
    49  
    50  		ctx, _ = testclock.UseTime(ctx, testclock.TestRecentTimeUTC)
    51  		ctx = secrets.Use(ctx, &testsecrets.Store{})
    52  		ctx = authtest.MockAuthConfig(ctx)
    53  
    54  		s := &Server{
    55  			HostnameProvider: func(string) string {
    56  				return "results.usercontent.example.com"
    57  			},
    58  		}
    59  		ctx = auth.WithState(ctx, &authtest.FakeState{
    60  			Identity: identity.AnonymousIdentity,
    61  		})
    62  
    63  		Convey(`Basic case`, func() {
    64  			url, exp, err := s.GenerateSignedURL(ctx, "request.example.com", "invocations/inv/artifacts/a")
    65  			So(err, ShouldBeNil)
    66  			So(url, ShouldStartWith, "https://results.usercontent.example.com/invocations/inv/artifacts/a?token=")
    67  			So(exp, ShouldResemble, clock.Now(ctx).UTC().Add(time.Hour))
    68  		})
    69  
    70  		Convey(`Escaped test id`, func() {
    71  			url, exp, err := s.GenerateSignedURL(ctx, "request.example.com", "invocations/inv/tests/t%2Ft/results/r/artifacts/a")
    72  			So(err, ShouldBeNil)
    73  			So(url, ShouldStartWith, "https://results.usercontent.example.com/invocations/inv/tests/t%2Ft/results/r/artifacts/a?token=")
    74  			So(exp, ShouldResemble, clock.Now(ctx).UTC().Add(time.Hour))
    75  		})
    76  	})
    77  }
    78  
    79  func TestServeContent(t *testing.T) {
    80  	Convey(`TestServeContent`, t, func(c C) {
    81  		ctx := testutil.SpannerTestContext(t)
    82  
    83  		ctx, _ = testclock.UseTime(ctx, testclock.TestRecentTimeUTC)
    84  		ctx = secrets.Use(ctx, &testsecrets.Store{})
    85  		ctx = authtest.MockAuthConfig(ctx)
    86  
    87  		casReader := &artifactcontenttest.FakeCASReader{
    88  			Res: []*bytestream.ReadResponse{
    89  				{Data: []byte("contents")},
    90  			},
    91  		}
    92  		var casReadErr error
    93  		s := &Server{
    94  			HostnameProvider: func(string) string {
    95  				return "example.com"
    96  			},
    97  			RBECASInstanceName: "projects/example/instances/artifacts",
    98  			ReadCASBlob: func(ctx context.Context, req *bytestream.ReadRequest) (bytestream.ByteStream_ReadClient, error) {
    99  				casReader.ReadLimit = int(req.ReadLimit)
   100  				return casReader, casReadErr
   101  			},
   102  		}
   103  
   104  		ctx = auth.WithState(ctx, &authtest.FakeState{
   105  			Identity: identity.AnonymousIdentity,
   106  		})
   107  
   108  		fetch := func(rawurl string) (res *http.Response, contents string) {
   109  			req, err := http.NewRequest("GET", rawurl, nil)
   110  			So(err, ShouldBeNil)
   111  			rec := httptest.NewRecorder()
   112  			s.handleGET(&router.Context{
   113  				Request: req.WithContext(ctx),
   114  				Writer:  rec,
   115  			})
   116  			res = rec.Result()
   117  			rawContents, err := io.ReadAll(res.Body)
   118  			So(err, ShouldBeNil)
   119  			defer res.Body.Close()
   120  			return res, string(rawContents)
   121  		}
   122  
   123  		newArt := func(parentID, artID, hash string, datas ...[]byte) {
   124  			casReader.Res = nil
   125  			sum := 0
   126  			for _, d := range datas {
   127  				casReader.Res = append(casReader.Res, &bytestream.ReadResponse{Data: d})
   128  				sum += len(d)
   129  			}
   130  			testutil.MustApply(ctx,
   131  				insert.Artifact("inv", parentID, artID, map[string]any{
   132  					"ContentType": "text/plain",
   133  					"Size":        sum,
   134  					"RBECASHash":  hash,
   135  				}),
   136  			)
   137  		}
   138  
   139  		testutil.MustApply(ctx, insert.Invocation("inv", pb.Invocation_FINALIZED, nil))
   140  
   141  		Convey(`Invalid resource name`, func() {
   142  			res, _ := fetch("https://results.usercontent.example.com/invocations/inv")
   143  			So(res.StatusCode, ShouldEqual, http.StatusBadRequest)
   144  		})
   145  
   146  		Convey(`Invalid token`, func() {
   147  			res, _ := fetch("https://results.usercontent.example.com/invocations/inv/artifacts/a?token=bad")
   148  			So(res.StatusCode, ShouldEqual, http.StatusForbidden)
   149  		})
   150  
   151  		Convey(`No token`, func() {
   152  			res, _ := fetch("https://results.usercontent.example.com/invocations/inv/artifacts/a")
   153  			So(res.StatusCode, ShouldEqual, http.StatusUnauthorized)
   154  		})
   155  
   156  		Convey(`Escaped test id`, func() {
   157  			newArt("tr/t/r", "a", "sha256:deadbeef", []byte("contents"))
   158  			u, _, err := s.GenerateSignedURL(ctx, "request.example.com", "invocations/inv/tests/t/results/r/artifacts/a")
   159  			So(err, ShouldBeNil)
   160  			res, actualContents := fetch(u)
   161  			So(res.StatusCode, ShouldEqual, http.StatusOK)
   162  			So(actualContents, ShouldEqual, "contents")
   163  		})
   164  
   165  		Convey(`limit`, func() {
   166  			newArt("tr/t/r", "a", "sha256:deadbeef", []byte("contents"))
   167  			u, _, err := s.GenerateSignedURL(ctx, "request.example.com", "invocations/inv/tests/t/results/r/artifacts/a")
   168  			So(err, ShouldBeNil)
   169  
   170  			Convey(`empty`, func() {
   171  				res, body := fetch(u + "&n=")
   172  				So(res.StatusCode, ShouldEqual, http.StatusOK)
   173  				So(body, ShouldEqual, "contents")
   174  				So(res.ContentLength, ShouldEqual, len("contents"))
   175  			})
   176  
   177  			Convey(`0`, func() {
   178  				res, body := fetch(u + "&n=0")
   179  				So(res.StatusCode, ShouldEqual, http.StatusOK)
   180  				So(body, ShouldEqual, "contents")
   181  				So(res.ContentLength, ShouldEqual, len("contents"))
   182  			})
   183  
   184  			Convey("limit < art_size", func() {
   185  				res, body := fetch(u + "&n=2")
   186  				So(res.StatusCode, ShouldEqual, http.StatusOK)
   187  				So(body, ShouldEqual, "co")
   188  				So(res.ContentLength, ShouldEqual, len("co"))
   189  			})
   190  
   191  			Convey("limit > art_size", func() {
   192  				res, body := fetch(u + "&n=100")
   193  				So(res.StatusCode, ShouldEqual, http.StatusOK)
   194  				So(body, ShouldEqual, "contents")
   195  				So(res.ContentLength, ShouldEqual, len("contents"))
   196  			})
   197  
   198  			Convey(`multiple`, func() {
   199  				res, body := fetch(u + "&n=4&n=23")
   200  				So(res.StatusCode, ShouldEqual, http.StatusOK)
   201  				So(body, ShouldEqual, "cont")
   202  				So(res.ContentLength, ShouldEqual, len("cont"))
   203  			})
   204  
   205  			Convey(`invalid`, func() {
   206  				res, _ := fetch(u + "&n=limit")
   207  				So(res.StatusCode, ShouldEqual, http.StatusBadRequest)
   208  			})
   209  		})
   210  
   211  		Convey(`E2E with RBE-CAS`, func() {
   212  			newArt("", "rbe", "sha256:deadbeef", []byte("first "), []byte("second"))
   213  			u, _, err := s.GenerateSignedURL(ctx, "request.example.com", "invocations/inv/artifacts/rbe")
   214  			So(err, ShouldBeNil)
   215  
   216  			Convey(`Not found`, func() {
   217  				casReadErr = status.Errorf(codes.NotFound, "not found")
   218  				res, _ := fetch(u)
   219  				So(res.StatusCode, ShouldEqual, http.StatusNotFound)
   220  			})
   221  
   222  			Convey(`Not found on first chunk`, func() {
   223  				casReader.ResErr = status.Errorf(codes.NotFound, "not found")
   224  				casReader.ResErrIndex = 0
   225  				res, _ := fetch(u)
   226  				So(res.StatusCode, ShouldEqual, http.StatusNotFound)
   227  			})
   228  
   229  			Convey(`Recv error`, func() {
   230  				casReader.ResErr = status.Errorf(codes.Internal, "internal error")
   231  				res, _ := fetch(u)
   232  				So(res.StatusCode, ShouldEqual, http.StatusInternalServerError)
   233  			})
   234  
   235  			Convey("Succeeds", func() {
   236  				res, body := fetch(u)
   237  				So(res.StatusCode, ShouldEqual, http.StatusOK)
   238  				So(body, ShouldEqual, "first second")
   239  				So(res.Header.Get("Content-Type"), ShouldEqual, "text/plain")
   240  				So(res.ContentLength, ShouldEqual, len("first second"))
   241  			})
   242  		})
   243  	})
   244  }