go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/config_service/rpc/get_project_configs_test.go (about) 1 // Copyright 2023 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 rpc 16 17 import ( 18 "crypto/sha256" 19 "encoding/hex" 20 "strings" 21 "testing" 22 23 "github.com/golang/mock/gomock" 24 "google.golang.org/genproto/protobuf/field_mask" 25 "google.golang.org/grpc/codes" 26 "google.golang.org/protobuf/encoding/prototext" 27 "google.golang.org/protobuf/proto" 28 29 "go.chromium.org/luci/auth/identity" 30 "go.chromium.org/luci/common/errors" 31 cfgcommonpb "go.chromium.org/luci/common/proto/config" 32 "go.chromium.org/luci/config" 33 "go.chromium.org/luci/gae/service/datastore" 34 "go.chromium.org/luci/server/auth" 35 "go.chromium.org/luci/server/auth/authtest" 36 37 "go.chromium.org/luci/config_service/internal/clients" 38 "go.chromium.org/luci/config_service/internal/common" 39 "go.chromium.org/luci/config_service/internal/model" 40 pb "go.chromium.org/luci/config_service/proto" 41 "go.chromium.org/luci/config_service/testutil" 42 43 . "github.com/smartystreets/goconvey/convey" 44 . "go.chromium.org/luci/common/testing/assertions" 45 ) 46 47 func TestGetProjectConfigs(t *testing.T) { 48 t.Parallel() 49 50 Convey("GetProjectConfigs", t, func() { 51 ctx := testutil.SetupContext() 52 srv := &Configs{} 53 54 userID := identity.Identity("user:user@example.com") 55 fakeAuthDB := authtest.NewFakeDB() 56 testutil.InjectSelfConfigs(ctx, map[string]proto.Message{ 57 common.ACLRegistryFilePath: &cfgcommonpb.AclCfg{ 58 ProjectAccessGroup: "project-access-group", 59 }, 60 }) 61 fakeAuthDB.AddMocks( 62 authtest.MockMembership(userID, "project-access-group"), 63 ) 64 ctx = auth.WithState(ctx, &authtest.FakeState{ 65 Identity: userID, 66 FakeDB: fakeAuthDB, 67 }) 68 69 // Inject "services/myservice" 70 testutil.InjectConfigSet(ctx, config.MustServiceSet("myservice"), nil) 71 72 // Inject "projects/project1" with a small "config.cfg" file and "other1.cfg" file. 73 configPb := &cfgcommonpb.ProjectCfg{Name: "config.cfg"} 74 configPbBytes, err := prototext.Marshal(configPb) 75 So(err, ShouldBeNil) 76 configPbSha := sha256.Sum256(configPbBytes) 77 configPbShaStr := hex.EncodeToString(configPbSha[:]) 78 testutil.InjectConfigSet(ctx, config.MustProjectSet("project1"), map[string]proto.Message{ 79 "config.cfg": configPb, 80 "other1.cfg": &cfgcommonpb.ProjectCfg{Name: "other1.cfg"}, 81 }) 82 83 // Inject "projects/project2" with a large "config.cfg" file and "other2.cfg" file. 84 testutil.InjectConfigSet(ctx, config.MustProjectSet("project2"), map[string]proto.Message{ 85 "other2.cfg": &cfgcommonpb.ProjectCfg{Name: "other2.cfg"}, 86 }) 87 So(datastore.Put(ctx, &model.File{ 88 Path: "config.cfg", 89 Revision: datastore.MakeKey(ctx, model.ConfigSetKind, "projects/project2", model.RevisionKind, "1"), 90 ContentSHA256: "configsha256", 91 GcsURI: "gs://bucket/configsha256", 92 Size: 1000, 93 }), ShouldBeNil) 94 ctl := gomock.NewController(t) 95 defer ctl.Finish() 96 mockGsClient := clients.NewMockGsClient(ctl) 97 ctx = clients.WithGsClient(ctx, mockGsClient) 98 99 Convey("invalid path", func() { 100 res, err := srv.GetProjectConfigs(ctx, &pb.GetProjectConfigsRequest{}) 101 So(res, ShouldBeNil) 102 So(err, ShouldHaveGRPCStatus, codes.InvalidArgument, `invalid path - "": not specified`) 103 104 res, err = srv.GetProjectConfigs(ctx, &pb.GetProjectConfigsRequest{Path: "/file"}) 105 So(res, ShouldBeNil) 106 So(err, ShouldHaveGRPCStatus, codes.InvalidArgument, `invalid path - "/file": must not be absolute`) 107 108 res, err = srv.GetProjectConfigs(ctx, &pb.GetProjectConfigsRequest{Path: "./file"}) 109 So(res, ShouldBeNil) 110 So(err, ShouldHaveGRPCStatus, codes.InvalidArgument, `invalid path - "./file": should not start with './' or '../'`) 111 }) 112 113 Convey("invalid mask", func() { 114 res, err := srv.GetProjectConfigs(ctx, &pb.GetProjectConfigsRequest{ 115 Path: "file", 116 Fields: &field_mask.FieldMask{ 117 Paths: []string{"random"}, 118 }, 119 }) 120 So(res, ShouldBeNil) 121 So(err, ShouldHaveGRPCStatus, codes.InvalidArgument, `invalid fields mask: field "random" does not exist in message Config`) 122 }) 123 124 Convey("no access to matched files", func() { 125 ctx = auth.WithState(ctx, &authtest.FakeState{ 126 Identity: identity.Identity("user:random@example.com"), 127 FakeDB: fakeAuthDB, 128 }) 129 res, err := srv.GetProjectConfigs(ctx, &pb.GetProjectConfigsRequest{ 130 Path: "config.cfg", 131 }) 132 So(err, ShouldBeNil) 133 So(res, ShouldResembleProto, &pb.GetProjectConfigsResponse{}) 134 }) 135 136 Convey("no matched files", func() { 137 res, err := srv.GetProjectConfigs(ctx, &pb.GetProjectConfigsRequest{ 138 Path: "non_exist.cfg", 139 }) 140 So(err, ShouldBeNil) 141 So(res, ShouldResembleProto, &pb.GetProjectConfigsResponse{}) 142 }) 143 144 Convey("found", func() { 145 mockGsClient.EXPECT().SignedURL( 146 gomock.Eq("bucket"), 147 gomock.Eq("configsha256"), 148 gomock.Any(), 149 ).Return("signed_url", nil) 150 151 res, err := srv.GetProjectConfigs(ctx, &pb.GetProjectConfigsRequest{ 152 Path: "config.cfg", 153 }) 154 So(err, ShouldBeNil) 155 So(res, ShouldResembleProto, &pb.GetProjectConfigsResponse{ 156 Configs: []*pb.Config{ 157 { 158 ConfigSet: "projects/project1", 159 Path: "config.cfg", 160 Content: &pb.Config_RawContent{ 161 RawContent: configPbBytes, 162 }, 163 ContentSha256: configPbShaStr, 164 Revision: "1", 165 Size: int64(len(configPbBytes)), 166 }, 167 { 168 ConfigSet: "projects/project2", 169 Path: "config.cfg", 170 Content: &pb.Config_SignedUrl{ 171 SignedUrl: "signed_url", 172 }, 173 ContentSha256: "configsha256", 174 Revision: "1", 175 Size: 1000, 176 }, 177 }, 178 }) 179 }) 180 181 Convey(" config size > maxRawContentSize", func() { 182 fooPb := &cfgcommonpb.ProjectCfg{Name: strings.Repeat("0123456789", maxRawContentSize/10)} 183 fooPbBytes, err := prototext.Marshal(fooPb) 184 So(err, ShouldBeNil) 185 fooPbSha := sha256.Sum256(fooPbBytes) 186 fooPbShaStr := hex.EncodeToString(fooPbSha[:]) 187 188 testutil.InjectConfigSet(ctx, config.MustProjectSet("foo"), map[string]proto.Message{ 189 "foo.cfg": fooPb, 190 }) 191 mockGsClient.EXPECT().SignedURL( 192 gomock.Eq(testutil.TestGsBucket), 193 gomock.Eq("foo.cfg"), 194 gomock.Any(), 195 ).Return("signed_url", nil) 196 197 res, err := srv.GetProjectConfigs(ctx, &pb.GetProjectConfigsRequest{ 198 Path: "foo.cfg", 199 }) 200 So(err, ShouldBeNil) 201 So(res, ShouldResembleProto, &pb.GetProjectConfigsResponse{ 202 Configs: []*pb.Config{ 203 { 204 ConfigSet: "projects/foo", 205 Path: "foo.cfg", 206 Content: &pb.Config_SignedUrl{ 207 SignedUrl: "signed_url", 208 }, 209 ContentSha256: fooPbShaStr, 210 Revision: "1", 211 Size: int64(len(fooPbBytes)), 212 }, 213 }, 214 }) 215 }) 216 217 Convey("total size > maxProjConfigsResSize", func() { 218 originalLimit := maxProjConfigsResSize 219 // Make the limit to 1 byte to avoid taking too much memory to test this 220 // use case. 221 maxProjConfigsResSize = 1 222 defer func() { maxProjConfigsResSize = originalLimit }() 223 224 mockGsClient.EXPECT().SignedURL( 225 gomock.Eq(testutil.TestGsBucket), 226 gomock.Eq("config.cfg"), 227 gomock.Any(), 228 ).Return("signed_url1", nil) 229 mockGsClient.EXPECT().SignedURL( 230 gomock.Eq("bucket"), 231 gomock.Eq("configsha256"), 232 gomock.Any(), 233 ).Return("signed_url2", nil) 234 235 res, err := srv.GetProjectConfigs(ctx, &pb.GetProjectConfigsRequest{ 236 Path: "config.cfg", 237 }) 238 So(err, ShouldBeNil) 239 So(res, ShouldResembleProto, &pb.GetProjectConfigsResponse{ 240 Configs: []*pb.Config{ 241 { 242 ConfigSet: "projects/project1", 243 Path: "config.cfg", 244 Content: &pb.Config_SignedUrl{ 245 SignedUrl: "signed_url1", 246 }, 247 ContentSha256: configPbShaStr, 248 Revision: "1", 249 Size: int64(len(configPbBytes)), 250 }, 251 { 252 ConfigSet: "projects/project2", 253 Path: "config.cfg", 254 Content: &pb.Config_SignedUrl{ 255 SignedUrl: "signed_url2", 256 }, 257 ContentSha256: "configsha256", 258 Revision: "1", 259 Size: 1000, 260 }, 261 }, 262 }) 263 }) 264 265 Convey("mask", func() { 266 res, err := srv.GetProjectConfigs(ctx, &pb.GetProjectConfigsRequest{ 267 Path: "other1.cfg", 268 Fields: &field_mask.FieldMask{ 269 Paths: []string{"config_set", "path"}, 270 }, 271 }) 272 273 So(err, ShouldBeNil) 274 So(res, ShouldResembleProto, &pb.GetProjectConfigsResponse{ 275 Configs: []*pb.Config{ 276 { 277 ConfigSet: "projects/project1", 278 Path: "other1.cfg", 279 }, 280 }, 281 }) 282 }) 283 284 Convey("GCS error on signed url", func() { 285 mockGsClient.EXPECT().SignedURL( 286 gomock.Eq("bucket"), 287 gomock.Eq("configsha256"), 288 gomock.Any(), 289 ).Return("", errors.New("GCS internal error")) 290 291 res, err := srv.GetProjectConfigs(ctx, &pb.GetProjectConfigsRequest{ 292 Path: "config.cfg", 293 }) 294 So(res, ShouldBeNil) 295 So(err, ShouldHaveGRPCStatus, codes.Internal, "error while generating the config signed url") 296 }) 297 }) 298 }