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 }