|
7 | 7 | "testing" |
8 | 8 |
|
9 | 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" |
| 10 | + utl "github.com/vmware/terraform-provider-nsxt/api/utl" |
10 | 11 | "github.com/vmware/vsphere-automation-sdk-go/services/nsxt/model" |
11 | 12 | ) |
12 | 13 |
|
@@ -390,6 +391,99 @@ func TestAttachRulesByParentPathGatewayPolicy(t *testing.T) { |
390 | 391 | }) |
391 | 392 | } |
392 | 393 |
|
| 394 | +func TestGetQueryStringVPCScopedToProjectNotVPC(t *testing.T) { |
| 395 | + // VPCID must be omitted from the cache bucket key: NSX policy paths/IDs are unique |
| 396 | + // within a project across all VPCs, and narrowing the key (and the underlying search) |
| 397 | + // to a single VPC caused a fresh cache bucket per VPC, regressing cache mode below |
| 398 | + // no-cache performance for VPC-scoped resource types touching many VPCs. |
| 399 | + for _, clientType := range []utl.ClientType{utl.VPC, utl.Multitenancy} { |
| 400 | + context := utl.SessionContext{ClientType: clientType, ProjectID: "proj-1", VPCID: "vpc-1"} |
| 401 | + got := getQueryString(resourceTypeVpcAttachment, context) |
| 402 | + if strings.Contains(got, "vpc-1") { |
| 403 | + t.Fatalf("clientType=%v: query %q must not be scoped to a specific VPCID", clientType, got) |
| 404 | + } |
| 405 | + if !strings.Contains(got, "proj-1") { |
| 406 | + t.Fatalf("clientType=%v: query %q must still be scoped to the project", got, got) |
| 407 | + } |
| 408 | + |
| 409 | + otherVPC := context |
| 410 | + otherVPC.VPCID = "vpc-2" |
| 411 | + if got2 := getQueryString(resourceTypeVpcAttachment, otherVPC); got2 != got { |
| 412 | + t.Fatalf("clientType=%v: query must be identical across VPCs in the same project so the cache bucket is shared; got %q vs %q", clientType, got, got2) |
| 413 | + } |
| 414 | + } |
| 415 | +} |
| 416 | + |
| 417 | +func TestProjectScopedSearchContextStripsVPCID(t *testing.T) { |
| 418 | + for _, clientType := range []utl.ClientType{utl.VPC, utl.Multitenancy} { |
| 419 | + in := utl.SessionContext{ClientType: clientType, ProjectID: "proj-1", VPCID: "vpc-1"} |
| 420 | + out := projectScopedSearchContext(in) |
| 421 | + if out.VPCID != "" { |
| 422 | + t.Fatalf("clientType=%v: expected VPCID stripped, got %q", clientType, out.VPCID) |
| 423 | + } |
| 424 | + if out.ProjectID != "proj-1" { |
| 425 | + t.Fatalf("clientType=%v: ProjectID must be preserved, got %q", clientType, out.ProjectID) |
| 426 | + } |
| 427 | + } |
| 428 | + |
| 429 | + // Non-VPC-scoped contexts must be returned unchanged. |
| 430 | + local := utl.SessionContext{ClientType: utl.Local, ProjectID: "", VPCID: ""} |
| 431 | + if got := projectScopedSearchContext(local); got != local { |
| 432 | + t.Fatalf("Local context should be unchanged, got %+v", got) |
| 433 | + } |
| 434 | +} |
| 435 | + |
| 436 | +func TestShouldIndexByPathForVPCScopedTypes(t *testing.T) { |
| 437 | + // VPC-scoped types must key by path, not short id: NSX ids for these types (often |
| 438 | + // user-chosen via nsx_id) are only guaranteed unique within their own VPC, but the cache |
| 439 | + // populate search/bucket for these types is now shared across all VPCs in a project. |
| 440 | + vpcScopedTypes := []string{ |
| 441 | + resourceTypeVpc, resourceTypeVpcAttachment, resourceTypeVpcConnectivityProfile, |
| 442 | + resourceTypeVpcIpAddressAllocation, resourceTypeVpcServiceProfile, resourceTypeVpcSubnet, |
| 443 | + resourceTypeTransitGateway, resourceTypeTransitGatewayAttachment, |
| 444 | + resourceTypeProjectIpAddressAllocation, resourceTypePolicyVpcNatRule, |
| 445 | + // Also reachable via CacheAwareResourceRead under a VPC-scoped SessionContext |
| 446 | + // (resource_nsxt_vpc_group.go, resource_nsxt_vpc_gateway_policy.go, |
| 447 | + // resource_nsxt_vpc_static_routes.go, resource_nsxt_vpc_dhcp_v4_static_binding_config.go), |
| 448 | + // so they need the same path-indexing safety even though some are shared with |
| 449 | + // non-VPC-scoped sibling resources (GatewayPolicy, StaticRoutes, DhcpV4StaticBindingConfig). |
| 450 | + resourceTypeVPCGroup, resourceTypeGatewayPolicy, resourceTypeStaticRoutes, resourceTypeDhcpV4StaticBindingConfig, |
| 451 | + } |
| 452 | + for _, rt := range vpcScopedTypes { |
| 453 | + if !shouldIndexByPath(rt) { |
| 454 | + t.Errorf("shouldIndexByPath(%q) = false, want true", rt) |
| 455 | + } |
| 456 | + } |
| 457 | +} |
| 458 | + |
| 459 | +func TestConverListToMapByTypeVpcScopedResourcesIndexedByPath(t *testing.T) { |
| 460 | + // Two different VPCs in the same project can legitimately have a VpcSubnet with the same |
| 461 | + // user-chosen short id (getOrGenerateID2 only checks uniqueness within the current VPC). |
| 462 | + // Since the cache bucket for VPC-scoped types is now shared project-wide, both objects |
| 463 | + // land in the same map; this test confirms each remains independently retrievable via its |
| 464 | + // distinct (project/system-unique) path, even though they share a colliding short id. |
| 465 | + pathA := "/orgs/o/projects/p/vpcs/vpcA/subnets/subnet1" |
| 466 | + pathB := "/orgs/o/projects/p/vpcs/vpcB/subnets/subnet1" |
| 467 | + subnetA := model.VpcSubnet{Id: strPtr("subnet1"), DisplayName: strPtr("subnet1-a"), Path: strPtr(pathA)} |
| 468 | + subnetB := model.VpcSubnet{Id: strPtr("subnet1"), DisplayName: strPtr("subnet1-b"), Path: strPtr(pathB)} |
| 469 | + |
| 470 | + svs, err := modelsToStructValues([]model.VpcSubnet{subnetA, subnetB}, model.VpcSubnetBindingType()) |
| 471 | + if err != nil { |
| 472 | + t.Fatalf("modelsToStructValues: %v", err) |
| 473 | + } |
| 474 | + |
| 475 | + got := converListToMapByType(svs, resourceTypeVpcSubnet) |
| 476 | + if got == nil { |
| 477 | + t.Fatal("converListToMapByType returned nil") |
| 478 | + } |
| 479 | + if got[pathA] == nil { |
| 480 | + t.Errorf("VPC A's subnet not retrievable by its path %q", pathA) |
| 481 | + } |
| 482 | + if got[pathB] == nil { |
| 483 | + t.Errorf("VPC B's subnet not retrievable by its path %q", pathB) |
| 484 | + } |
| 485 | +} |
| 486 | + |
393 | 487 | func TestErrCacheUseBackendDirect(t *testing.T) { |
394 | 488 | if !errors.Is(errCacheUseBackendDirect, errCacheUseBackendDirect) { |
395 | 489 | t.Fatal("errors.Is should match sentinel to itself") |
@@ -437,3 +531,149 @@ func TestReflectStringField(t *testing.T) { |
437 | 531 | } |
438 | 532 | }) |
439 | 533 | } |
| 534 | + |
| 535 | +func TestCacheAwareDataSourceReadByIDBypassesCacheForShortIDOnPathIndexedTypes(t *testing.T) { |
| 536 | + dsSchema := map[string]*schema.Schema{ |
| 537 | + "id": getDataSourceIDSchema(), |
| 538 | + "display_name": getDataSourceExtendedDisplayNameSchema(), |
| 539 | + "description": getDataSourceDescriptionSchema(), |
| 540 | + "path": getPathSchema(), |
| 541 | + } |
| 542 | + m := nsxtClients{CommonConfig: commonProviderConfig{CacheMode: "config_scope"}} |
| 543 | + |
| 544 | + t.Run("short-id-on-path-indexed-type-bypasses-cache", func(t *testing.T) { |
| 545 | + d := schema.TestResourceDataRaw(t, dsSchema, map[string]interface{}{"id": "subnet1"}) |
| 546 | + _, ok := cacheAwareDataSourceReadByID[model.VpcSubnet](d, m, nil, "subnet1", resourceTypeVpcSubnet, model.VpcSubnetBindingType()) |
| 547 | + if ok { |
| 548 | + t.Fatal("expected cache bypass (ok=false) for a short id on a path-indexed resource type") |
| 549 | + } |
| 550 | + if d.Id() != "" { |
| 551 | + t.Fatalf("expected d.Id() to be untouched on bypass, got %q", d.Id()) |
| 552 | + } |
| 553 | + }) |
| 554 | + |
| 555 | + t.Run("full-path-on-path-indexed-type-not-bypassed-by-this-check", func(t *testing.T) { |
| 556 | + // A full path contains "/", so the new short-id bypass must not trigger; this proves |
| 557 | + // the check is specific to short (non-path) ids, not to shouldIndexByPath types broadly. |
| 558 | + path := "/orgs/o/projects/p/vpcs/vpcA/subnets/subnet1" |
| 559 | + d := schema.TestResourceDataRaw(t, dsSchema, map[string]interface{}{"id": path}) |
| 560 | + if _, ok := postWriteByKey.LoadAndDelete(postWriteKey(resourceTypeVpcSubnet, path)); ok { |
| 561 | + t.Fatal("test setup: unexpected post-write marker present") |
| 562 | + } |
| 563 | + postWriteByKey.Store(postWriteKey(resourceTypeVpcSubnet, path), struct{}{}) |
| 564 | + _, ok := cacheAwareDataSourceReadByID[model.VpcSubnet](d, m, nil, path, resourceTypeVpcSubnet, model.VpcSubnetBindingType()) |
| 565 | + if ok { |
| 566 | + t.Fatal("expected ok=false (post-write bypass), proving control reached the postWriteByKey check rather than the short-id bypass") |
| 567 | + } |
| 568 | + }) |
| 569 | +} |
| 570 | + |
| 571 | +func TestCacheAwareResourceReadBypassesCacheForShortIDOnPathIndexedTypes(t *testing.T) { |
| 572 | + // CacheAwareResourceRead's resourceID is a short id (not yet path) during the Create-then-Read |
| 573 | + // sequence (path isn't set on d until the Read populates it from the live object) and after |
| 574 | + // terraform import (importers call d.SetId(shortID) without setting path). Without this bypass, |
| 575 | + // such a call falls through to gcache.readCache keyed by the short id against the shared |
| 576 | + // project-wide bucket, which can return a different VPC's same-short-id object. |
| 577 | + rSchema := map[string]*schema.Schema{ |
| 578 | + "path": getPathSchema(), |
| 579 | + } |
| 580 | + m := nsxtClients{CommonConfig: commonProviderConfig{CacheMode: "config_scope"}} |
| 581 | + |
| 582 | + t.Run("short-id-on-path-indexed-type-bypasses-cache-and-calls-backendRead", func(t *testing.T) { |
| 583 | + d := schema.TestResourceDataRaw(t, rSchema, map[string]interface{}{}) |
| 584 | + d.SetId("subnet1") |
| 585 | + backendReadCalled := false |
| 586 | + obj, cacheUsed, cacheAttempted, err := CacheAwareResourceRead[model.VpcSubnet]( |
| 587 | + d, m, nil, "subnet1", resourceTypeVpcSubnet, model.VpcSubnetBindingType(), |
| 588 | + func() (*model.VpcSubnet, error) { |
| 589 | + backendReadCalled = true |
| 590 | + return &model.VpcSubnet{Id: strPtr("subnet1")}, nil |
| 591 | + }, |
| 592 | + func(*model.VpcSubnet) error { return nil }, |
| 593 | + ) |
| 594 | + if err != nil { |
| 595 | + t.Fatalf("unexpected error: %v", err) |
| 596 | + } |
| 597 | + if !backendReadCalled { |
| 598 | + t.Fatal("expected backendRead to be called when bypassing cache for a short id on a path-indexed type") |
| 599 | + } |
| 600 | + if cacheUsed { |
| 601 | + t.Fatal("expected cacheUsed=false when bypassing cache") |
| 602 | + } |
| 603 | + if cacheAttempted { |
| 604 | + t.Fatal("expected cacheAttempted=false: the short-id bypass should skip the cache attempt entirely, not count as a failed attempt") |
| 605 | + } |
| 606 | + if obj == nil || obj.Id == nil || *obj.Id != "subnet1" { |
| 607 | + t.Fatalf("expected backendRead's object to be returned, got %+v", obj) |
| 608 | + } |
| 609 | + }) |
| 610 | + |
| 611 | + t.Run("full-path-on-path-indexed-type-not-bypassed-by-this-check", func(t *testing.T) { |
| 612 | + path := "/orgs/o/projects/p/vpcs/vpcA/subnets/subnet1" |
| 613 | + d := schema.TestResourceDataRaw(t, rSchema, map[string]interface{}{"path": path}) |
| 614 | + d.SetId(path) |
| 615 | + if _, ok := postWriteByKey.LoadAndDelete(postWriteKey(resourceTypeVpcSubnet, path)); ok { |
| 616 | + t.Fatal("test setup: unexpected post-write marker present") |
| 617 | + } |
| 618 | + postWriteByKey.Store(postWriteKey(resourceTypeVpcSubnet, path), struct{}{}) |
| 619 | + _, cacheUsed, cacheAttempted, err := CacheAwareResourceRead[model.VpcSubnet]( |
| 620 | + d, m, nil, path, resourceTypeVpcSubnet, model.VpcSubnetBindingType(), |
| 621 | + func() (*model.VpcSubnet, error) { return &model.VpcSubnet{Id: strPtr("subnet1")}, nil }, |
| 622 | + func(*model.VpcSubnet) error { return nil }, |
| 623 | + ) |
| 624 | + if err != nil { |
| 625 | + t.Fatalf("unexpected error: %v", err) |
| 626 | + } |
| 627 | + if cacheUsed { |
| 628 | + t.Fatal("expected cacheUsed=false (post-write bypass path, not a cache hit)") |
| 629 | + } |
| 630 | + if !cacheAttempted { |
| 631 | + t.Fatal("expected cacheAttempted=true, proving control reached the postWriteByKey check rather than the short-id bypass") |
| 632 | + } |
| 633 | + }) |
| 634 | +} |
| 635 | + |
| 636 | +func TestTryCacheReadBypassesCacheForShortIDOnPathIndexedTypes(t *testing.T) { |
| 637 | + rSchema := map[string]*schema.Schema{ |
| 638 | + "path": getPathSchema(), |
| 639 | + } |
| 640 | + m := nsxtClients{CommonConfig: commonProviderConfig{CacheMode: "config_scope"}} |
| 641 | + |
| 642 | + t.Run("short-id-on-path-indexed-type-bypasses-cache", func(t *testing.T) { |
| 643 | + d := schema.TestResourceDataRaw(t, rSchema, map[string]interface{}{}) |
| 644 | + d.SetId("subnet1") |
| 645 | + obj, cacheUsed, cacheAttempted, err := TryCacheRead[model.VpcSubnet](d, m, nil, "subnet1", resourceTypeVpcSubnet, model.VpcSubnetBindingType()) |
| 646 | + if err != nil { |
| 647 | + t.Fatalf("unexpected error: %v", err) |
| 648 | + } |
| 649 | + if obj != nil { |
| 650 | + t.Fatalf("expected nil object on bypass, got %+v", obj) |
| 651 | + } |
| 652 | + if cacheUsed { |
| 653 | + t.Fatal("expected cacheUsed=false when bypassing cache") |
| 654 | + } |
| 655 | + if cacheAttempted { |
| 656 | + t.Fatal("expected cacheAttempted=false: the short-id bypass should skip the cache attempt entirely") |
| 657 | + } |
| 658 | + }) |
| 659 | + |
| 660 | + t.Run("full-path-on-path-indexed-type-not-bypassed-by-this-check", func(t *testing.T) { |
| 661 | + path := "/orgs/o/projects/p/vpcs/vpcA/subnets/subnet1" |
| 662 | + d := schema.TestResourceDataRaw(t, rSchema, map[string]interface{}{"path": path}) |
| 663 | + d.SetId(path) |
| 664 | + if _, ok := postWriteByKey.LoadAndDelete(postWriteKey(resourceTypeVpcSubnet, path)); ok { |
| 665 | + t.Fatal("test setup: unexpected post-write marker present") |
| 666 | + } |
| 667 | + postWriteByKey.Store(postWriteKey(resourceTypeVpcSubnet, path), struct{}{}) |
| 668 | + _, cacheUsed, cacheAttempted, err := TryCacheRead[model.VpcSubnet](d, m, nil, path, resourceTypeVpcSubnet, model.VpcSubnetBindingType()) |
| 669 | + if err != nil { |
| 670 | + t.Fatalf("unexpected error: %v", err) |
| 671 | + } |
| 672 | + if cacheUsed { |
| 673 | + t.Fatal("expected cacheUsed=false (post-write bypass path, not a cache hit)") |
| 674 | + } |
| 675 | + if !cacheAttempted { |
| 676 | + t.Fatal("expected cacheAttempted=true, proving control reached the postWriteByKey check rather than the short-id bypass") |
| 677 | + } |
| 678 | + }) |
| 679 | +} |
0 commit comments