go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/gce/appengine/rpc/config_test.go (about) 1 // Copyright 2018 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 "context" 19 "testing" 20 21 "google.golang.org/genproto/protobuf/field_mask" 22 "google.golang.org/protobuf/types/known/emptypb" 23 24 "go.chromium.org/luci/gae/impl/memory" 25 "go.chromium.org/luci/gae/service/datastore" 26 "go.chromium.org/luci/gce/api/config/v1" 27 "go.chromium.org/luci/gce/appengine/model" 28 "go.chromium.org/luci/server/auth" 29 "go.chromium.org/luci/server/auth/authtest" 30 31 . "github.com/smartystreets/goconvey/convey" 32 33 . "go.chromium.org/luci/common/testing/assertions" 34 ) 35 36 func TestConfig(t *testing.T) { 37 t.Parallel() 38 39 Convey("Config", t, func() { 40 srv := &Config{} 41 c := memory.Use(context.Background()) 42 datastore.GetTestable(c).AutoIndex(true) 43 datastore.GetTestable(c).Consistent(true) 44 45 Convey("Delete", func() { 46 Convey("invalid", func() { 47 Convey("nil", func() { 48 cfg, err := srv.Delete(c, nil) 49 So(err, ShouldErrLike, "ID is required") 50 So(cfg, ShouldBeNil) 51 }) 52 53 Convey("empty", func() { 54 cfg, err := srv.Delete(c, &config.DeleteRequest{}) 55 So(err, ShouldErrLike, "ID is required") 56 So(cfg, ShouldBeNil) 57 }) 58 }) 59 60 Convey("valid", func() { 61 So(datastore.Put(c, &model.Config{ 62 ID: "id", 63 }), ShouldBeNil) 64 cfg, err := srv.Delete(c, &config.DeleteRequest{ 65 Id: "id", 66 }) 67 So(err, ShouldBeNil) 68 So(cfg, ShouldResemble, &emptypb.Empty{}) 69 err = datastore.Get(c, &model.Config{ 70 ID: "id", 71 }) 72 So(err, ShouldEqual, datastore.ErrNoSuchEntity) 73 }) 74 }) 75 76 Convey("Ensure", func() { 77 Convey("invalid", func() { 78 Convey("nil", func() { 79 cfg, err := srv.Ensure(c, nil) 80 So(err, ShouldErrLike, "ID is required") 81 So(cfg, ShouldBeNil) 82 }) 83 84 Convey("empty", func() { 85 cfg, err := srv.Ensure(c, &config.EnsureRequest{}) 86 So(err, ShouldErrLike, "ID is required") 87 So(cfg, ShouldBeNil) 88 }) 89 90 Convey("ID", func() { 91 cfg, err := srv.Ensure(c, &config.EnsureRequest{ 92 Config: &config.Config{}, 93 }) 94 So(err, ShouldErrLike, "ID is required") 95 So(cfg, ShouldBeNil) 96 }) 97 }) 98 99 Convey("valid", func() { 100 Convey("new", func() { 101 cfg, err := srv.Ensure(c, &config.EnsureRequest{ 102 Id: "id", 103 Config: &config.Config{ 104 Attributes: &config.VM{ 105 Disk: []*config.Disk{ 106 {}, 107 }, 108 MachineType: "type", 109 NetworkInterface: []*config.NetworkInterface{ 110 {}, 111 }, 112 Project: "project", 113 Zone: "zone", 114 }, 115 Lifetime: &config.TimePeriod{ 116 Time: &config.TimePeriod_Seconds{ 117 Seconds: 3600, 118 }, 119 }, 120 Prefix: "prefix", 121 }, 122 }) 123 So(err, ShouldBeNil) 124 So(cfg, ShouldResembleProto, &config.Config{ 125 Attributes: &config.VM{ 126 Disk: []*config.Disk{ 127 {}, 128 }, 129 MachineType: "type", 130 Project: "project", 131 NetworkInterface: []*config.NetworkInterface{ 132 {}, 133 }, 134 Zone: "zone", 135 }, 136 Lifetime: &config.TimePeriod{ 137 Time: &config.TimePeriod_Seconds{ 138 Seconds: 3600, 139 }, 140 }, 141 Prefix: "prefix", 142 }) 143 }) 144 145 Convey("update doesn't erase currentAmount", func() { 146 So(datastore.Put(c, &model.Config{ 147 ID: "id", 148 Config: &config.Config{ 149 Amount: &config.Amount{ 150 Max: 20, 151 Min: 10, 152 }, 153 CurrentAmount: 15, 154 Owner: []string{ 155 "owners", 156 }, 157 Prefix: "prefix", 158 }, 159 }), ShouldBeNil) 160 161 cfg, err := srv.Ensure(c, &config.EnsureRequest{ 162 Id: "id", 163 Config: &config.Config{ 164 Amount: &config.Amount{ 165 Max: 100, 166 Min: 50, 167 }, 168 Attributes: &config.VM{ 169 Disk: []*config.Disk{ 170 {}, 171 }, 172 MachineType: "type", 173 NetworkInterface: []*config.NetworkInterface{ 174 {}, 175 }, 176 Project: "project", 177 Zone: "zone", 178 }, 179 Lifetime: &config.TimePeriod{ 180 Time: &config.TimePeriod_Seconds{ 181 Seconds: 3600, 182 }, 183 }, 184 Prefix: "prefix", 185 }, 186 }) 187 So(err, ShouldBeNil) 188 So(cfg, ShouldResembleProto, &config.Config{ 189 Amount: &config.Amount{ 190 Max: 100, 191 Min: 50, 192 }, 193 CurrentAmount: 15, // Same as before. 194 Attributes: &config.VM{ 195 Disk: []*config.Disk{ 196 {}, 197 }, 198 MachineType: "type", 199 Project: "project", 200 NetworkInterface: []*config.NetworkInterface{ 201 {}, 202 }, 203 Zone: "zone", 204 }, 205 Lifetime: &config.TimePeriod{ 206 Time: &config.TimePeriod_Seconds{ 207 Seconds: 3600, 208 }, 209 }, 210 Prefix: "prefix", 211 }) 212 }) 213 }) 214 }) 215 216 Convey("Get", func() { 217 Convey("invalid", func() { 218 Convey("nil", func() { 219 cfg, err := srv.Get(c, nil) 220 So(err, ShouldErrLike, "ID is required") 221 So(cfg, ShouldBeNil) 222 }) 223 224 Convey("empty", func() { 225 cfg, err := srv.Get(c, &config.GetRequest{}) 226 So(err, ShouldErrLike, "ID is required") 227 So(cfg, ShouldBeNil) 228 }) 229 230 Convey("unauthorized owners", func() { 231 c = auth.WithState(c, &authtest.FakeState{ 232 IdentityGroups: []string{"owners1"}, 233 }) 234 So(datastore.Put(c, &model.Config{ 235 ID: "id", 236 Config: &config.Config{ 237 Prefix: "prefix", 238 Owner: []string{ 239 "owners2", 240 }, 241 }, 242 }), ShouldBeNil) 243 cfg, err := srv.Get(c, &config.GetRequest{ 244 Id: "id", 245 }) 246 So(err, ShouldErrLike, "no config found with ID \"id\" or unauthorized user") 247 So(cfg, ShouldBeNil) 248 }) 249 }) 250 251 Convey("valid", func() { 252 Convey("not found", func() { 253 cfg, err := srv.Get(c, &config.GetRequest{ 254 Id: "id", 255 }) 256 So(err, ShouldErrLike, "no config found with ID \"id\" or unauthorized user") 257 So(cfg, ShouldBeNil) 258 }) 259 260 Convey("found", func() { 261 c = auth.WithState(c, &authtest.FakeState{ 262 IdentityGroups: []string{"owners"}, 263 }) 264 So(datastore.Put(c, &model.Config{ 265 ID: "id", 266 Config: &config.Config{ 267 Prefix: "prefix", 268 Owner: []string{ 269 "owners", 270 }, 271 }, 272 }), ShouldBeNil) 273 cfg, err := srv.Get(c, &config.GetRequest{ 274 Id: "id", 275 }) 276 So(err, ShouldBeNil) 277 So(cfg, ShouldResembleProto, &config.Config{ 278 Prefix: "prefix", 279 Owner: []string{ 280 "owners", 281 }, 282 }) 283 }) 284 }) 285 }) 286 287 Convey("List", func() { 288 Convey("invalid", func() { 289 Convey("page token", func() { 290 req := &config.ListRequest{ 291 PageToken: "token", 292 } 293 _, err := srv.List(c, req) 294 So(err, ShouldErrLike, "invalid page token") 295 }) 296 }) 297 298 Convey("valid", func() { 299 Convey("nil", func() { 300 Convey("none", func() { 301 rsp, err := srv.List(c, nil) 302 So(err, ShouldBeNil) 303 So(rsp.Configs, ShouldBeEmpty) 304 }) 305 306 Convey("one", func() { 307 So(datastore.Put(c, &model.Config{ 308 ID: "id", 309 }), ShouldBeNil) 310 rsp, err := srv.List(c, nil) 311 So(err, ShouldBeNil) 312 So(rsp.Configs, ShouldHaveLength, 1) 313 }) 314 }) 315 316 Convey("empty", func() { 317 Convey("none", func() { 318 req := &config.ListRequest{} 319 rsp, err := srv.List(c, req) 320 So(err, ShouldBeNil) 321 So(rsp.Configs, ShouldBeEmpty) 322 }) 323 324 Convey("one", func() { 325 So(datastore.Put(c, &model.Config{ 326 ID: "id", 327 }), ShouldBeNil) 328 req := &config.ListRequest{} 329 rsp, err := srv.List(c, req) 330 So(err, ShouldBeNil) 331 So(rsp.Configs, ShouldHaveLength, 1) 332 }) 333 }) 334 335 Convey("pages", func() { 336 So(datastore.Put(c, &model.Config{ID: "id1"}), ShouldBeNil) 337 So(datastore.Put(c, &model.Config{ID: "id2"}), ShouldBeNil) 338 So(datastore.Put(c, &model.Config{ID: "id3"}), ShouldBeNil) 339 340 Convey("default", func() { 341 req := &config.ListRequest{} 342 rsp, err := srv.List(c, req) 343 So(err, ShouldBeNil) 344 So(rsp.Configs, ShouldNotBeEmpty) 345 }) 346 347 Convey("one", func() { 348 req := &config.ListRequest{ 349 PageSize: 1, 350 } 351 rsp, err := srv.List(c, req) 352 So(err, ShouldBeNil) 353 So(rsp.Configs, ShouldHaveLength, 1) 354 So(rsp.NextPageToken, ShouldNotBeEmpty) 355 356 req.PageToken = rsp.NextPageToken 357 rsp, err = srv.List(c, req) 358 So(err, ShouldBeNil) 359 So(rsp.Configs, ShouldHaveLength, 1) 360 361 req.PageToken = rsp.NextPageToken 362 rsp, err = srv.List(c, req) 363 So(err, ShouldBeNil) 364 So(rsp.Configs, ShouldHaveLength, 1) 365 So(rsp.NextPageToken, ShouldBeEmpty) 366 }) 367 368 Convey("two", func() { 369 req := &config.ListRequest{ 370 PageSize: 2, 371 } 372 rsp, err := srv.List(c, req) 373 So(err, ShouldBeNil) 374 So(rsp.Configs, ShouldHaveLength, 2) 375 So(rsp.NextPageToken, ShouldNotBeEmpty) 376 377 req.PageToken = rsp.NextPageToken 378 rsp, err = srv.List(c, req) 379 So(err, ShouldBeNil) 380 So(rsp.Configs, ShouldHaveLength, 1) 381 So(rsp.NextPageToken, ShouldBeEmpty) 382 }) 383 384 Convey("many", func() { 385 req := &config.ListRequest{ 386 PageSize: 200, 387 } 388 rsp, err := srv.List(c, req) 389 So(err, ShouldBeNil) 390 So(rsp.Configs, ShouldHaveLength, 3) 391 So(rsp.NextPageToken, ShouldBeEmpty) 392 }) 393 }) 394 }) 395 }) 396 397 Convey("Update", func() { 398 c = auth.WithState(c, &authtest.FakeState{ 399 IdentityGroups: []string{"owners"}, 400 }) 401 So(datastore.Put(c, &model.Config{ 402 ID: "id", 403 Config: &config.Config{ 404 Amount: &config.Amount{ 405 Max: 3, 406 Min: 1, 407 }, 408 CurrentAmount: 1, 409 Owner: []string{ 410 "owners", 411 }, 412 Prefix: "prefix", 413 Duts: map[string]*emptypb.Empty{"dut1": {}}, 414 }, 415 }), ShouldBeNil) 416 417 Convey("invalid", func() { 418 Convey("nil", func() { 419 cfg, err := srv.Update(c, nil) 420 So(err, ShouldErrLike, "ID is required") 421 So(cfg, ShouldBeNil) 422 mdl := &model.Config{ 423 ID: "id", 424 } 425 err = datastore.Get(c, mdl) 426 So(err, ShouldBeNil) 427 So(mdl.Config.CurrentAmount, ShouldEqual, 1) 428 }) 429 430 Convey("empty", func() { 431 cfg, err := srv.Update(c, &config.UpdateRequest{}) 432 So(err, ShouldErrLike, "ID is required") 433 So(cfg, ShouldBeNil) 434 mdl := &model.Config{ 435 ID: "id", 436 } 437 err = datastore.Get(c, mdl) 438 So(err, ShouldBeNil) 439 So(mdl.Config.CurrentAmount, ShouldEqual, 1) 440 }) 441 442 Convey("id", func() { 443 cfg, err := srv.Update(c, &config.UpdateRequest{ 444 UpdateMask: &field_mask.FieldMask{ 445 Paths: []string{ 446 "config.current_amount", 447 }, 448 }, 449 }) 450 So(err, ShouldErrLike, "ID is required") 451 So(cfg, ShouldBeNil) 452 mdl := &model.Config{ 453 ID: "id", 454 } 455 err = datastore.Get(c, mdl) 456 So(err, ShouldBeNil) 457 So(mdl.Config.CurrentAmount, ShouldEqual, 1) 458 }) 459 460 Convey("update mask", func() { 461 Convey("missing", func() { 462 cfg, err := srv.Update(c, &config.UpdateRequest{ 463 Id: "id", 464 }) 465 So(err, ShouldErrLike, "update mask is required") 466 So(cfg, ShouldBeNil) 467 mdl := &model.Config{ 468 ID: "id", 469 } 470 err = datastore.Get(c, mdl) 471 So(err, ShouldBeNil) 472 So(mdl.Config.CurrentAmount, ShouldEqual, 1) 473 }) 474 475 Convey("empty", func() { 476 cfg, err := srv.Update(c, &config.UpdateRequest{ 477 Id: "id", 478 UpdateMask: &field_mask.FieldMask{ 479 Paths: []string{}, 480 }, 481 }) 482 So(err, ShouldErrLike, "update mask is required") 483 So(cfg, ShouldBeNil) 484 mdl := &model.Config{ 485 ID: "id", 486 } 487 err = datastore.Get(c, mdl) 488 So(err, ShouldBeNil) 489 So(mdl.Config.CurrentAmount, ShouldEqual, 1) 490 }) 491 492 Convey("invalid mask", func() { 493 cfg, err := srv.Update(c, &config.UpdateRequest{ 494 Id: "id", 495 UpdateMask: &field_mask.FieldMask{ 496 Paths: []string{ 497 "config.amount.default", 498 }, 499 }, 500 }) 501 So(err, ShouldErrLike, "invalid or immutable") 502 So(cfg, ShouldBeNil) 503 mdl := &model.Config{ 504 ID: "id", 505 } 506 err = datastore.Get(c, mdl) 507 So(err, ShouldBeNil) 508 So(mdl.Config.CurrentAmount, ShouldEqual, 1) 509 }) 510 511 Convey("immutable field", func() { 512 cfg, err := srv.Update(c, &config.UpdateRequest{ 513 Id: "id", 514 UpdateMask: &field_mask.FieldMask{ 515 Paths: []string{ 516 "config.prefix", 517 }, 518 }, 519 }) 520 So(err, ShouldErrLike, "invalid or immutable") 521 So(cfg, ShouldBeNil) 522 mdl := &model.Config{ 523 ID: "id", 524 } 525 err = datastore.Get(c, mdl) 526 So(err, ShouldBeNil) 527 So(mdl.Config.CurrentAmount, ShouldEqual, 1) 528 }) 529 }) 530 }) 531 532 Convey("valid", func() { 533 Convey("unauthorized", func() { 534 c = auth.WithState(c, &authtest.FakeState{}) 535 cfg, err := srv.Update(c, &config.UpdateRequest{ 536 Id: "id", 537 Config: &config.Config{ 538 CurrentAmount: 2, 539 }, 540 UpdateMask: &field_mask.FieldMask{ 541 Paths: []string{ 542 "config.current_amount", 543 }, 544 }, 545 }) 546 So(err, ShouldErrLike, "unauthorized user") 547 So(cfg, ShouldBeNil) 548 mdl := &model.Config{ 549 ID: "id", 550 } 551 err = datastore.Get(c, mdl) 552 So(err, ShouldBeNil) 553 So(mdl.Config.CurrentAmount, ShouldEqual, 1) 554 }) 555 556 Convey("authorized", func() { 557 Convey("CurrentAmount", func() { 558 Convey("min", func() { 559 cfg, err := srv.Update(c, &config.UpdateRequest{ 560 Id: "id", 561 UpdateMask: &field_mask.FieldMask{ 562 Paths: []string{ 563 "config.current_amount", 564 }, 565 }, 566 }) 567 So(err, ShouldBeNil) 568 So(cfg.CurrentAmount, ShouldEqual, 1) 569 mdl := &model.Config{ 570 ID: "id", 571 } 572 err = datastore.Get(c, mdl) 573 So(err, ShouldBeNil) 574 So(mdl.Config.CurrentAmount, ShouldEqual, 1) 575 }) 576 577 Convey("max", func() { 578 cfg, err := srv.Update(c, &config.UpdateRequest{ 579 Id: "id", 580 Config: &config.Config{ 581 CurrentAmount: 4, 582 }, 583 UpdateMask: &field_mask.FieldMask{ 584 Paths: []string{ 585 "config.current_amount", 586 }, 587 }, 588 }) 589 So(err, ShouldBeNil) 590 So(cfg.CurrentAmount, ShouldEqual, 3) 591 mdl := &model.Config{ 592 ID: "id", 593 } 594 err = datastore.Get(c, mdl) 595 So(err, ShouldBeNil) 596 So(mdl.Config.CurrentAmount, ShouldEqual, 3) 597 }) 598 599 Convey("updates", func() { 600 cfg, err := srv.Update(c, &config.UpdateRequest{ 601 Id: "id", 602 Config: &config.Config{ 603 CurrentAmount: 2, 604 }, 605 UpdateMask: &field_mask.FieldMask{ 606 Paths: []string{ 607 "config.current_amount", 608 }, 609 }, 610 }) 611 So(err, ShouldBeNil) 612 So(cfg.CurrentAmount, ShouldEqual, 2) 613 mdl := &model.Config{ 614 ID: "id", 615 } 616 err = datastore.Get(c, mdl) 617 So(err, ShouldBeNil) 618 So(mdl.Config.CurrentAmount, ShouldEqual, 2) 619 }) 620 }) 621 Convey("duts", func() { 622 Convey("updates", func() { 623 cfg, err := srv.Update(c, &config.UpdateRequest{ 624 Id: "id", 625 Config: &config.Config{ 626 Duts: map[string]*emptypb.Empty{ 627 "hello": {}, 628 "world": {}, 629 }, 630 }, 631 UpdateMask: &field_mask.FieldMask{ 632 Paths: []string{ 633 "config.duts", 634 }, 635 }, 636 }) 637 So(err, ShouldBeNil) 638 So(cfg.CurrentAmount, ShouldEqual, 2) 639 So(cfg.Duts, ShouldResembleProto, map[string]*emptypb.Empty{ 640 "hello": {}, 641 "world": {}, 642 }) 643 mdl := &model.Config{ 644 ID: "id", 645 } 646 err = datastore.Get(c, mdl) 647 So(err, ShouldBeNil) 648 So(mdl.Config.CurrentAmount, ShouldEqual, 2) 649 So(mdl.Config.Duts, ShouldResembleProto, map[string]*emptypb.Empty{ 650 "hello": {}, 651 "world": {}, 652 }) 653 }) 654 }) 655 }) 656 }) 657 }) 658 }) 659 Convey("equalDuts", t, func() { 660 Convey("fail", func() { 661 Convey("different length", func() { 662 s1 := map[string]*emptypb.Empty{ 663 "dut1": {}, 664 } 665 s2 := map[string]*emptypb.Empty{ 666 "dut1": {}, 667 "dut2": {}, 668 } 669 isEqual := dutsEqual(s1, s2) 670 So(isEqual, ShouldBeFalse) 671 }) 672 Convey("different keys", func() { 673 s1 := map[string]*emptypb.Empty{ 674 "dut1": {}, 675 } 676 s2 := map[string]*emptypb.Empty{ 677 "dut2": {}, 678 } 679 isEqual := dutsEqual(s1, s2) 680 So(isEqual, ShouldBeFalse) 681 }) 682 }) 683 Convey("pass", func() { 684 Convey("same keys", func() { 685 s1 := map[string]*emptypb.Empty{ 686 "dut1": {}, 687 "dut2": {}, 688 } 689 s2 := map[string]*emptypb.Empty{ 690 "dut1": {}, 691 "dut2": {}, 692 } 693 isEqual := dutsEqual(s1, s2) 694 So(isEqual, ShouldBeTrue) 695 }) 696 Convey("empty", func() { 697 s1 := map[string]*emptypb.Empty{} 698 s2 := map[string]*emptypb.Empty{} 699 isEqual := dutsEqual(s1, s2) 700 So(isEqual, ShouldBeTrue) 701 }) 702 }) 703 }) 704 }